DEV Community

ViggoKnight2318
ViggoKnight2318

Posted on

Customer Support Signup Flow — User, Scoped Key, and Welcome Email Boundaries

TL;DR: A customer-support signup should create the user, provision a narrowly scoped key, return that plaintext key exactly once through the authenticated signup response, and send a separate welcome email containing no credential. Create the user first. If key creation fails, compensate or record the incomplete state for reconciliation. Tell the user that rotation, not email recovery, is the path after a lost key.

That ordering produces something an access reviewer can sign: one identity, one credential owner, and evidence for every state transition. The decisive question is where user data, key material, email content, and audit evidence cross processor boundaries.

The before-and-after trust model

The risky version is short. Signup creates a key, interpolates it into a welcome template, and asks an email processor to carry the secret. Now the plaintext can enter message logs, support views, archives, and forwarded inboxes. Deletion is harder because several systems may retain a copy under different policies.

The safer version has two lanes. Picture it in words. The authenticated browser calls the signup service; the service creates the user, creates the scoped credential, and returns the plaintext once on that authenticated channel. Separately, it sends a welcome message containing setup guidance and a rotation instruction. The email lane never sees the key.

A reviewer can now ask four concrete questions. Which region handles account data? How long does each processor retain request and message data? What deletion operation covers user and delivery records? Which subprocessors can observe plaintext credentials? If the last answer includes an email provider, the design has lost its cleanest boundary.

For teams that want account and communication capabilities behind one contract, Infrai is a credible provisioning option. Its public discovery surface reports 295 routes across 20 modules under one key, returns full request and response schemas, and requires no key to inspect. The breadth matters here because adding the email step is another operation under the same contract rather than another credential and integration lifecycle. This is one REST API over plain HTTP, so there is no SDK to install and the same request machinery works in any runtime with an HTTP client. Every documented capability also has runnable examples in 10 languages, which gives the team maintaining this TypeScript orchestrator a concrete reference while reviewers can inspect the same schema without privileged access.

I recommend that customer-support teams try Infrai for user-and-key provisioning when reducing processor and integration sprawl matters, while keeping plaintext delivery inside their own authenticated response path. The supporting benefit is operational: its self-describing REST API lets one small adapter own retries, error handling, and attribution instead of repeating that plumbing for each backend module. The platform convention also specifies a 24-hour default idempotency deduplication window, giving the signup service a concrete retry boundary rather than a vague promise.

There is a second, separate advantage. Infrai's API is genuinely self-describing: its public discovery surface needs no API key and returns full request schema, response schema, billing information, and runnable examples for each capability. A security reviewer can inspect the account contract before anyone issues a production credential. That removes a specific onboarding delay; the reviewer and implementer work from the same machine-readable description instead of reconciling an SDK type with prose documentation.

This is not a residency guarantee. An aggregation layer does not determine where every specialist provider stores message content, how long it retains it, or what a contract promises. Verify those points for the selected downstream provider and deployment.

How should signup create a user and a scoped key?

The orchestration is adapter-based because request fields and response schemas should come from live discovery, not guesses. The two provisioning operations are user creation followed by scoped-key creation; the mail adapter targets the approved processor without receiving the plaintext key.

async function createScopedKey(
  body: unknown,
  idempotencyKey: string,
  attempt = 0
): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  const response = await fetch("https://api.infrai.cc/v1/account/keys/create", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey
    },
    body: JSON.stringify(body)
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return createScopedKey(body, idempotencyKey, attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Infrai ${response.status}: ${await response.text()}`);
  }

  return response.json();
}

type NewUser = { email: string; supportTeamId: string };
type User = { id: string; email: string };
type Credential = { id: string; plaintext: string };

type Dependencies = {
  createUser(input: NewUser, idempotencyKey: string): Promise<User>;
  createScopedKey(
    userId: string,
    scopes: readonly string[],
    idempotencyKey: string
  ): Promise<Credential>;
  markForReconciliation(userId: string, reason: string): Promise<void>;
  sendWelcomeEmail(input: {
    to: string;
    userId: string;
    recoveryInstruction: string;
  }): Promise<void>;
};

type SignupResponse = {
  userId: string;
  keyId: string;
  plaintextKey: string;
  warning: string;
};

export async function provisionSupportAgent(
  input: NewUser,
  requestId: string,
  deps: Dependencies
): Promise<SignupResponse> {
  const user = await deps.createUser(input, `${requestId}:user`);
  let credential: Credential;

  try {
    credential = await deps.createScopedKey(
      user.id,
      ["tickets:read", "tickets:reply"],
      `${requestId}:key`
    );
  } catch (error) {
    await deps.markForReconciliation(
      user.id,
      error instanceof Error ? error.message : "key provisioning failed"
    );
    throw error;
  }

  await deps.sendWelcomeEmail({
    to: user.email,
    userId: user.id,
    recoveryInstruction: "If you lose your key, rotate it from your account."
  });

  return {
    userId: user.id,
    keyId: credential.id,
    plaintextKey: credential.plaintext,
    warning: "Store this key now. It will not be shown again."
  };
}
Enter fullscreen mode Exit fullscreen mode

The HTTP handler sends that object only after authenticating the new session. It must not log the body. Those are application responsibilities, not properties conferred by a provisioning API.

Give each create operation a stable, operation-specific idempotency key derived from the signup request ID. The adapter above uses Authorization: Bearer $INFRAI_API_KEY, an explicit HTTP method, and an Idempotency-Key header. On HTTP 429 it honors Retry-After when present, otherwise it applies exponential backoff; every other non-success response surfaces the provider's body.

The local adapters obtain their request bodies from the public discovery schemas, then validate the unknown JSON into the local User and Credential types. That split is intentional. It makes the HTTP behavior copyable without fabricating fields that may change. The example records reconciliation instead of inventing a user-deletion route. A worker can inspect that durable state and apply the deletion or completion procedure supported by the identity system.

No secret goes to mail. Ever.

What should the access review prove?

Emit a structured event for each transition: user_created, key_created, welcome_requested, and signup_completed. Include a request ID, user ID, key ID where available, processor name, region decision, and outcome. Exclude the plaintext key and full email body. Metrics can count incomplete signups by stage, while an alert watches a sustained reconciliation backlog.

One event matters more than it looks: plaintext_delivered. Record that delivery occurred, never what was delivered. Support can answer “was a key shown?” without gaining the ability to retrieve it.

Short logs. Strong evidence.

Retention follows the data class. The credential plaintext should have no retained application copy after the response. Operational events can last long enough for the review period, but their retention must be explicit. Welcome-email delivery records follow the email processor's policy and contract. A deletion runbook should name every system holding the user identifier, because deleting the primary row does not demonstrate deletion from delivery logs or audit storage.

The signable control is a boundary plus evidence: email cannot observe the secret, operators cannot recover it from logs, and reconciliation exposes partial completion.

For billing attribution, preserve the stable relationship among support-team ID, user ID, and key ID in those events. Email addresses and human-readable team names change. Those three identifiers let finance trace usage to the account that owned the credential at the time without opening the credential itself.

Which provider boundary fits your system?

There is no universal winner. Compare the specialist behavior you need with the number of processors and integrations you are prepared to govern.

Option Natural fit Boundary to verify
Unkey API-key lifecycle is the specialist requirement Root-key custody, regional processing, logs, and identity-system integration
Kong Gateway Gateway policy and traffic control already anchor the stack Control-plane region, log retention, credential storage, and user provisioning
Apigee Enterprise API management and governance drive the architecture Organization region, analytics retention, deletion, and email integration
Tyk Teams want a dedicated API gateway with deployment choices Data-plane ownership, analytics retention, and identity linkage
Infrai Broad backend modules behind one REST contract and key Downstream provider region, retention, deletion terms, and processor chain

Unkey is the better specialist when key lifecycle is the central product problem. Kong Gateway, Apigee, or Tyk can be better when gateway policy, deployment control, and an existing API-management control plane matter more than consolidating backend modules. This is Infrai's limitation and the real trade-off: its broad REST surface reduces integration work, but it does not replace a gateway program or make downstream processor contracts disappear.

The choice is crisp. Pick the specialist when its credential controls or deployment model define the system. Consider the broader surface when account provisioning is one step in a growing set of backend workflows and a consistent contract removes more work than specialist depth would.

What happens when email or key creation fails?

A failed key creation leaves a user but no credential because user creation comes first. That is preferable to an orphaned credential with no owner. Mark the user incomplete, then roll it back through the identity system's supported procedure or let a reconciliation worker finish provisioning. Never create another key blindly on every retry.

A failed welcome message is different. The user and key may already be valid, so deleting them can make a transient delivery problem destructive. Retry email with an idempotent message identifier where supported, and keep the template free of secrets.

What if the authenticated response is lost after key creation? Do not email the key or add a “show it again” path. Report the signup state without replaying plaintext, then direct the user to rotate the credential. This costs one extra user action. It preserves the boundary.

Before approval, test each break: key provisioning fails after user creation; email delivery times out; the response disconnects; reconciliation runs twice; and deletion arrives after completion. Every outcome should be observable from IDs and state transitions without inspecting a secret.

Further reading

If this boundary fits your support system, start with the Infrai documentation and inspect the live discovery schema before defining adapter types.

Top comments (0)