AUTH4D-150 verification email wording

Developers can set four plain-text fields for the verification code emails that an application sends: the sender display name, the subject, a short intro and a footer. They are read and updated through the application routes from AUTH4D-18 (repository file docs/api-spec/auth4d-18.md), next to branding and device_settings from AUTH4D-56 (repository file docs/api-spec/auth4d-56.md). The wire shapes are VerificationEmailSchema and VerificationEmailUpdateSchema in packages/contracts. Values are stored in the clients columns added by migration 0013. Browser and game applications both have them.

The verification code, the line that introduces it, and the purpose, expiry and “ignore this email” sentence are fixed and cannot be replaced. The sender address is the deployment’s EMAIL_FROM address and never changes; only the display name in front of it does. A project’s own sending domain is covered by AUTH4D-168 (repository file docs/api-spec/auth4d-168.md).

Read

GET /management/tenants/{tenantId}/clients/{clientId} (scope client:read, any project role)

{
  "client": { "clientId": "cli_…", "clientType": "native", "…": "…" },
  "branding": { "support_url": "https://help.example.com/" },
  "device_settings": { "user_code_length": 8, "code_ttl_seconds": 600, "poll_interval_seconds": 5 },
  "verification_email": {
    "sender_name": "Starfall Arena",
    "subject": "Your Starfall Arena sign-in code",
    "intro": "Welcome back, pilot!",
    "footer": "Starfall Arena is made by Example Games Ltd."
  }
}

verification_email contains only the configured fields. An application without wording returns {} and its emails use the built-in text. The list route (GET …/clients) is unchanged.

Update

PATCH (or PUT) /management/tenants/{tenantId}/clients/{clientId} (scope client:write, project role owner or admin; a viewer gets 403 insufficient_role)

{ "verification_email": { "subject": "Your Starfall Arena sign-in code", "footer": null } }
  • The object is optional and partial, and it can be combined with branding, device_settings and the AUTH4D-18 fields. Omitted fields stay unchanged, null clears a field, and an object that is present must contain at least one field.
  • Leading and trailing whitespace is trimmed before validation and storage.
  • The response has the same shape as the read response and is sent with no-store.
  • Changes are committed in the same D1 batch as the other presentation fields, writing only the columns that change. Re-sending the same values is a no-op and writes no audit event.
Field Limit Extra rule
sender_name 1–64 chars No < > { } " \ @ , ; : ( ) [ ]
subject 1–100 chars No < > { }
intro 1–300 chars No < > { }
footer 1–300 chars No < > { }

Every field must also be a single line without control, line-separator or text-direction characters, must not contain a link (:// or www.), and must not contain six or more digits in a row, so the text cannot pass for a verification code. Any failure returns 400 validation_failed with details[].field set to verification_email.<field>, and nothing is written. The D1 CHECKs repeat the length limits and the forbidden characters.

Rendering

packages/email renders the email. The wording is plain text: it is escaped in the HTML part, kept literal in the text part and never interpreted as a template. The layout, shared with the console preview through packages/email/src/verification-text.ts, is:

  1. intro, if set
  2. auth4.dev verification code: 381042 (fixed)
  3. Use this code to sign in to {application}. This code expires in N minutes. If you did not request it, you can ignore this email. (fixed)
  4. Need help? Contact {application} support: {support_url}, if a support URL is set (AUTH4D-62)
  5. footer, if set

subject replaces the default subject auth4.dev verification code. sender_name becomes the quoted display name of the From header, for example "Starfall Arena" <noreply@noreply.auth4.dev>.

The API reads the wording by tenant and client when it enqueues the email and carries it inside the encrypted outbox payload. Delivery checks every field against the contract again and drops any field that fails, so a bad stored value falls back to the built-in text rather than blocking sign-in. If long non-ASCII wording would make the encrypted payload exceed the outbox limit, the wording is dropped for that email and the built-in text is used. Control-plane (console) sign-in emails never use customer wording. A database without migration 0013 reads as unconfigured.

Audit

A change writes the existing management.client.presentation.updated event. changed_fields names the fields, for example verification_email.intro,verification_email.subject. The wording itself is never recorded.

Follow-up

Sending from a project’s own domain is covered by AUTH4D-168 (repository file docs/api-spec/auth4d-168.md).

Source: docs/api-spec/auth4d-150.md