Browser-local release gate
Commit first, send second
No address, OTP, verification link, message, database row, raw log, SMTP credential, provider secret, cookie, token or API key is requested or transmitted.
Use bounded states only. The output is a release gate and recovery brief for one controlled attempt, not a request for private support data.
- Release gate
- Truthful user claim
- Boundary
- Next action
- Exit condition
- Safety
Why delivery is not the first boundary
A mailbox-visible OTP proves that one message was generated and accepted somewhere. It does not prove that the account row, verification generation, tenant, normalized email identity, API origin, or code lookup path needed to consume that message exists.
- Message visible
- The delivery path crossed at least one sender and receiver boundary. Do not use it to infer database consistency.
- Account not found
- The verifier could not resolve the pending identity from the same internal registration reference.
- Code not found
- The verifier could not resolve the newest generation for that account, tenant, and purpose.
- Accepted once
- The controlled path proved one generation, one account, one use, and one terminal state.
The first ten minutes
- Freeze repeat sends and pick one controlled signup or OTP request with an exact UTC time and random internal correlation ID.
- Confirm whether the request committed a pending account and a newest verification generation before any send helper or worker started.
- Trace the message handoff from that same correlation ID. A provider message ID belongs to the attempt; it is not proof of the account lookup path.
- Submit the newest code once through the same production verification entry point and record only bounded states: account-found, generation-found, accepted, expired, reused, or rejected.
- Keep private data out of the evidence. Never post or log the address, OTP, link, token, full message, credential, database row, cookie, or complete request body.
The minimal repair shape
The dependable sequence is small: normalize the identity, create or find one pending account, persist one newest generation tied to that internal account ID, commit the transaction, then enqueue or send through a recovery-aware handoff. The user-facing claim should be the strongest state actually reached.
- If the commit failed, return a retryable registration failure and do not send a code.
- If the commit succeeded but the handoff failed, keep the account pending and expose a rate-limited resend or reconcile path.
- If the message was accepted but verification cannot find the account, compare signup and verification origin, tenant, database, normalization, and account-ID binding before changing mail configuration.
- If the newest code is accepted, invalidate older generations and prove a reused or expired code cannot activate the account again.
A still-open public report shows the useful shape of this failure: signup sends an OTP, but verification later reports that the account cannot be found. The public value is the boundary, not the private address or code: linkid issue 533.
When to use a transactional outbox
An outbox is useful when verification email should survive process restarts, deploys, worker crashes, temporary SMTP failures, or provider rate limits. Write the outbox row in the same transaction as the pending account and generation. After commit, a worker can claim the row, send the message, and store one bounded result.
begin transaction;
pending_account = upsert_pending_account(normalized_identity);
generation = create_verification_generation(pending_account.id, purpose);
insert_outbox("verification_email", generation.id, correlation_id);
commit;
worker_claims_outbox(correlation_id);
provider_result = send_verification_message(generation.id);
record_bounded_provider_state(correlation_id, provider_result);
The important rule is not the table name. The important rule is that the email side effect cannot outrun the state needed to verify the message.
Safe observability
Useful production evidence can stay privacy-safe. Prefer random correlation IDs, UTC timestamps, deployment revision, tenant label, route label, account-state class, generation-state class, handoff-state class, provider message ID presence, and terminal verification outcome.
Do not collect or publish a live OTP, address, action link, password-reset token, SMTP password, API key, cookie, authorization header, full message body, full request payload, or raw database row to debug this class of bug.
When DNS matters again
DNS and sender authentication matter after one controlled message exists and the verifier can resolve the same account and generation. At that point, use SPF, DKIM, DMARC, PTR, MTA-STS, provider events, receiver acceptance, and mailbox visibility to explain missing or rejected messages. Before then, the defect is in application truth, ordering, or lookup consistency.
Use the local verification-email triage tool for the broader application, provider, SMTP, and receiver map. It does not ask for an address, OTP, message, token, or credential.