zgba Network

Implementing a 2FA Login SMS OTP API in Postgres — Cancel Semantics

A healthtech signup flow has an awkward operational constraint: after a resend or cancel, the team must explain which verification link was valid without pretending it can pull an SMS back from a carrier. TL;DR: a 2FA login SMS OTP API should carry the message, while Postgres owns token validity, resend ordering, and cancellation. Accepted requests and authenticated delivery observations must correlate with that application record under failure. This advice does not apply to a notice that carries no authorization capability. A subscription renewal notice can use the same transport adapter, but it needs a separate policy and state model. Mixing it with signup verification makes an apparently harmless notice retry capable of changing authentication state. Here is the bounded production scenario I use for design review, not a claimed incident: a patient asks for a signup link, requests another while the first is delayed, then cancels enrollment before either observation reaches the application. The trace has 6 events: create, dispatch timeout, replacement request, late observation for the first command, cancel, then attempted use of both links. A success-only test says almost nothing about this evidence boundary. The invariant is stricter than “send the latest message.” At most one link may authorize signup, cancellation must invalidate it locally, and the retained record must explain every decision even when observations arrive late or twice. An app builder using Node.js can still use this Go worker pattern because the runtime sits outside the state-machine contract; no Node.js behavior is assumed here. Order matters. How should a 2FA login SMS OTP API handle repeat sends? Start with the audit question, because feature grids hide it. Given a subject, an operator should be able to reconstruct the authorization sequence, the dispatch attempts, and the observations without depending on a messaging dashboard. That record does not prove that a person read a message. It proves what the application authorized and what its integrations reported. Run that fixed trace through the integration. The expected authorization result is deterministic: neither link works after cancellation, and before cancellation only the replacement could have worked. This is a decision rule, not a vendor benchmark, and it tests support for correlation rather than treating a polished console as evidence. Keep two timelines because “canceled” is otherwise dangerously vague. The authorization timeline records active, superseded, canceled, and expired decisions. The delivery timeline records queued, attempted, accepted, and observed events. A downstream acceptance can arrive after local cancellation without contradicting the local record; it changes delivery evidence, never token validity. Review question Evidence owned by the application Required transport property Which link was usable? command ID, token digest, expiry, supersession none Was dispatch attempted? attempt ID, command ID, time stable acceptance reference Why was access denied? terminal authorization transition none What happened downstream? correlated, normalized observation exportable event data and an authentication mechanism Can an auditor replay the order? immutable transition times and actor/reason fields timestamps retained as observations, not authority One status can’t answer all five questions. Prove the state machine before connecting transport The preventative code path is a transition function that rejects stale events. It is intentionally independent of an SDK and contains no destination or raw token. In a real service, execute the accepted transition and its audit insert in one database transaction, then enforce the same transition rules again when a link is redeemed. package verification import ( “errors” “time” ) type Status string const ( Active Status = “active” Superseded Status = “superseded” Canceled Status = “canceled” Expired Status = “expired” ) type Command struct { ID string Status Status ExpiresAt time.Time Replacement string Transitioned time.Time } var ErrInvalidTransition = errors.New(“invalid verification transition”) func Supersede(old Command, replacementID string, now time.Time) (Command, error) { if old.Status != Active || replacementID == "" || !now.Before(old.ExpiresAt) { return old, ErrInvalidTransition } old.Status = Superseded old.Replacement = replacementID old.Transitioned = now.UTC() return old, nil } func Cancel(current Command, now time.Time) (Command, error) { if current.Status != Active { return current, ErrInvalidTransition } current.Status = Canceled current.Transitioned = now.UTC() return current, nil } The database needs a concurrency rule as well. Two resends racing through separate processes cannot both remain active. A serializable transaction or a schema constraint appropriate to the Postgres design must make that impossible; the exact constraint depends on whether the subject identifier is global, tenant-scoped, or purpose-scoped, so hard-coding one here would smuggle in a privacy and tenancy decision. The trade-off is real: stronger transaction isolation simplifies the invariant, while contention and retry behavior need capacity tests under the application’s expected burst. The dispatch side should consume a committed outbox record. A network timeout is ambiguous: it does not say whether a remote system accepted the request. Reusing the command ID as the stable application correlation key lets a worker reconcile that uncertainty without minting another verification capability. package verification import ( “context” “time” ) type Dispatch struct { CommandID string DestinationRef string Template string ExpiresAt time.Time } type Acceptance struct { Reference string AcceptedAt time.Time } type Transport interface { Send(context.Context, Dispatch) (Acceptance, error) } The destination reference should follow the organization’s privacy model; the evidence table does not need a raw phone number merely because the delivery adapter eventually resolves one. Raw verification secrets do not belong there either. Compare APIs by running the disagreement trace A buy-versus-build review should expose operating ownership, not collapse into a price column. Managed transport can take responsibility for network connectivity and return observations. The application still owns authorization semantics, retention policy, access controls, and reconciliation. Self-operating more of the path increases control over evidence shape, but it also puts delivery operations and their on-call burden on the platform team. This pattern does not fit a workflow whose compliance rule demands independently provable human receipt: an application transition, network acceptance, or delivery observation can’t supply that proof, so the compliance owner must define another mechanism before implementation. Boundary Managed transport More self-operated delivery Acceptance test Network handoff delegated under a contract operated by the team Can the system correlate every accepted attempt? Authorization application-owned application-owned Does a late event leave validity unchanged? Observation schema adapter normalizes an external schema team defines and operates it Can duplicate and reordered events be replayed? Evidence access export terms and retention matter storage and access are team duties Can evidence be retrieved without console access? Lock-in concentrated in request and event mappings concentrated in infrastructure and procedures Can the adapter change without changing token rules? Run the same trace against every transport under evaluation. Record request correlation, acceptance reference, event authentication method, duplicate behavior, out-of-order behavior, export format, retention controls, regional processing constraints, and documented limits. A gap that prevents the compliance owner from satisfying the evidence policy is disqualifying. Do not infer handset receipt from provider acceptance, and do not treat a delivery observation as proof of human attention. Capacity still matters, but monthly message count is a weak sizing input. Size the queue for the signup burst the product expects, then measure high-percentile queue age, commands approaching expiry before dispatch, uncertain outcomes awaiting reconciliation, resend rate, and observation lag. Tie paging to the signup SLO and its error budget. One failed downstream request is diagnostic data; sustained verification unavailability is the user-facing event. Keep renewal notices out of authorization state Subscription renewal notices deserve their own command type, deduplication window, content revision, and delivery objective. They may share a queue implementation and transport interface with signup verification, but they must not call Supersede, rotate a verification digest, or reactivate a canceled signup. This separation also makes evidence clearer: the operator can show that a commercial communication did not mutate an authentication decision. For email fallback, DKIM provides a different kind of evidence. RFC 6376 defines a domain-level signature over selected headers and the message body that a verifier can validate. It does not define signup authorization, resend precedence, cancellation, or proof that a recipient acted. Use it for its stated boundary. If an AI agent can request a renewal notice or verification message, expose a narrow business command with an explicit input schema. The tool-use model described in Anthropic’s documentation separates a tool definition from the application’s execution of that tool; policy validation and the durable record must remain in application code. An agent request is input to the state machine, not audit authority. Operate the evidence path as part of the SLO Deploy the ledger and transition checks before enabling dispatch. First run the outbox consumer with external sends disabled and verify that the six-event trace produces the intended record. Then enable a bounded cohort, reconcile acceptances and observations, and test rollback. Rollback may stop new dispatch; it must never make a superseded or canceled link active again. This is where my capacity-planning reflex overrides a tidy feature comparison. A transport integration that looks excellent in a demo can still be a poor fit if its observation delay consumes most of the verification lifetime, its evidence cannot be exported, or the team cannot operate reconciliation within the signup SLO. Conversely, a richer event vocabulary has little value if the application cannot map it to a small, stable internal model. The decision rule is plain: use a delivery boundary whose evidence you can authenticate, correlate, export, and operate, while keeping authorization decisions under transactional application control. For healthtech signup, that is more defensible than nominal send latency or a long checklist. The limitation should remain explicit: if a legal obligation requires proof of notice, a delivery record alone cannot establish human receipt or attention, and the compliance owner must specify additional evidence. Sources https://datatracker.ietf.org/doc/html/rfc6376 https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview

View original article