Short answer: A defensible passwordless phone login in Express Node.js has an SMS OTP flow with a server-side resend cooldown, a max-attempt counter, and an audit record; the same evidence boundary covers a marketplace order receipt after payment settles.
A marketplace does not need the fanciest notification stack to send an order receipt after payment settles. It needs a small, inspectable workflow that can answer three questions later: what was sent, to whom, and what did the transport report?
The least complex option is an outbox row written in the same database transaction as the settled payment, followed by an asynchronous sender with idempotency and an append-only delivery record. That gives the compliance team evidence without pretending that an accepted message equals a read message.
| Choice | Useful evidence | Main liability |
|---|---|---|
| Inline send from the payment request | A quick provider response | Payment latency and ambiguous retries |
| Database outbox plus worker | Local intent, attempts, responses, timestamps | Requires a worker and retention policy |
| Full event platform | Routing and replay at larger scale | More configuration and more places to inspect |
My choice is the outbox. It is boring, which is exactly what an audit trail should be. The decision turns on evidence quality and failure handling, not the lowest message price.
How should passwordless phone login work in Express Node.js with SMS OTP?
The food delivery courier login case adds the familiar OTP controls, but the control flow is the same. Create a challenge with a short expiry, store only a verifier for the OTP, and bind the challenge to a normalized phone identity. Express should return the same public response for an unknown phone and a known phone. That limits account enumeration.
Resend is a new attempt to deliver the same challenge, not permission to create unlimited challenges. Enforce a cooldown, a maximum resend count, and a maximum verification-attempt count per challenge. Add an anti-abuse limit per phone fingerprint, IP range, and device signal, with policy review for false positives. A rejected resend should still produce an audit event.
The courier login is not the marketplace receipt itself. Treat it as the entry point that establishes an authenticated actor; keep the order receipt’s settlement event and delivery evidence in the outbox described below.
What does a receipt need to prove?
Start by separating business truth from transport truth. The payment service owns the fact that order ord_4821 settled for USD 38.40. The messaging worker owns the fact that it attempted to deliver a receipt to the customer’s recorded destination. A provider callback may later report acceptance, delivery, expiration, or failure. Those are different events.
A useful record contains a stable event ID, order ID, settlement timestamp, template revision, destination fingerprint, channel, attempt number, provider message ID when available, response status, and callback timestamps. Do not store a phone number in every log line. A keyed digest can correlate records while limiting accidental exposure. The exact retention period belongs in the organization’s policy and legal review; it should not be invented in application code.
This distinction fixes a common audit failure. A 200 from a submission endpoint proves that the request was accepted by that boundary. It does not prove handset delivery, and it certainly does not prove that a human read the receipt.
Why is the outbox worth the extra moving part?
A payment request should not wait on a remote messaging service. If the payment commits and the process dies before a separate send call, an inline design loses the receipt intent. If the send succeeds and the process dies before marking it complete, a retry can duplicate the message. The outbox makes the first boundary durable, then makes duplicate suppression explicit.
The critical key is not the provider’s message ID. It is a business idempotency key such as receipt:ord_4821:v3. A worker claims pending rows with a lease, sends one attempt, and records the result. A retry uses the same key and a provider-supported idempotency mechanism where one exists; otherwise, the database must make the uncertainty visible instead of claiming exactly-once delivery.
Here is the small part I want tested in isolation. It is an interface, not a vendor SDK.
type ReceiptJob = {
eventId: string;
orderId: string;
idempotencyKey: string;
templateRevision: string;
destination: string;
};
type SendResult = {
outcome: "accepted" | "rejected";
providerMessageId?: string;
responseCode?: string;
};
interface ReceiptTransport {
send(job: ReceiptJob): Promise<SendResult>;
}
async function deliverReceipt(
job: ReceiptJob,
transport: ReceiptTransport,
records: { append: (event: object) => Promise<void> },
): Promise<void> {
const result = await transport.send(job);
await records.append({
eventId: job.eventId,
orderId: job.orderId,
idempotencyKey: job.idempotencyKey,
templateRevision: job.templateRevision,
outcome: result.outcome,
providerMessageId: result.providerMessageId ?? null,
responseCode: result.responseCode ?? null,
recordedAt: new Date().toISOString(),
});
}
The production version also needs a claim lease, bounded retries, and a dead-letter state. The example intentionally leaves those policies outside the transport interface. Configuration bloat often hides the one decision that matters: which state transition is allowed after each result.
Where do delivery providers differ?
The provider is an adapter boundary, not the compliance model. Twilio exposes messaging APIs and documents SMS segmentation rules. Amazon SES is primarily an email service, so it fits an email receipt path rather than an SMS transport. Vonage exposes an SMS API. Those differences affect channel coverage and payload rules; they do not remove the need for a local outbox or an evidence ledger.
I would compare adapters on four concrete behaviors: whether request deduplication is supported, how callbacks authenticate, which delivery states are exposed, and how long message identifiers remain queryable. A feature matrix that omits those questions is marketing material with columns.
SMS also has a payload trap. GSM-7 and UCS-2 use different character limits, and a receipt containing a currency symbol or non-ASCII customer data can segment differently. Segmentation changes the number of transport units and can alter operational cost and delivery behavior. Keep receipt content short, render it from a versioned template, and test the actual encoded payload. Email has its own boundary: a submission response is not inbox placement.
The failure cases I would put in CI
I benchmark the state machine, not just the send call. A useful test set is small but adversarial: payment commits while the worker is down; a timeout occurs after the provider may have accepted the request; a callback arrives before the submission response is persisted; the same callback is delivered twice; and the template revision changes between two attempts. Each case should leave a deterministic audit record.
Metrics should distinguish intent count, attempt count, accepted count, callback delivery count, and terminal failure count. Alert on a growing pending age and on repeated uncertainty, not only on HTTP errors. A provider can be healthy while your callback endpoint is rejecting signatures or your lease duration is too short.\n\nThe runner-up is inline sending. It is reasonable for a low-risk informational notification where losing the message is acceptable and the request path has a strict latency budget. It is a poor fit for a payment receipt whose existence may need to be demonstrated later. A full event platform becomes the better choice when several teams need replay, routing, and independent retention controls. That is an organizational threshold, not a badge of maturity.
The decision rule is straightforward: persist the receipt intent with settlement, send asynchronously, record provider evidence separately, and describe the limits of that evidence plainly. No design can prove that a recipient read a message from a transport callback. It can prove what your system decided, attempted, and observed.
This approach is not suitable when the product has no durable payment record or cannot operate a worker; the trade-off is less evidence in exchange for less operational machinery. For a regulated marketplace, that trade-off is usually backwards.

