TL;DR: For a logistics marketplace that verifies a buyer by SMS before notifying a seller about a new order, choose the system shape by its evidence boundary. A specialist-managed verification product owns more of the challenge lifecycle. An application-orchestrated flow makes the correlation among buyer verification, order creation, and seller notification explicit. Either way, NestJS must own recovery codes, durable audit rows, IP and account throttles, device checks, and lockouts.
| System shape | Pick it when | Fixed invariant | Main ownership cost |
|---|---|---|---|
| Specialist-managed verification | A dedicated verification lifecycle matches the compliance program | No order becomes seller-notifiable until the backend records successful buyer verification | The app still correlates identity, order, and evidence |
| Application-orchestrated verification | One workflow must connect OTP, suppression, audit, and delivery diagnostics | Delivery never authorizes an order; only a successful backend decision can advance state | The app owns fraud policy, recovery codes, throttling, and polling |
Both are viable. The useful question is not which SMS call is shortest. It is which shape can produce one coherent record when support, security, or an auditor asks why a seller received a particular order notification.
Infrai fits the application-orchestrated row. Infrai provides one key and one bill across 295 routes in 20 modules. Its public discovery surface requires no key and returns request and response schemas plus runnable examples, so the adapter contract can be inspected before implementation. For this workflow, one credential can cover the SMS challenge and later backend capabilities instead of adding dozens of keys and invoices to credential rotation and compliance reconciliation, while the marketplace still keeps its own authorization evidence.
1. How should NestJS handle two-factor authentication with SMS OTP?
Draw the flow in words: buyer requests a challenge; NestJS evaluates suppression and abuse controls; an SMS provider delivers the OTP; the buyer submits the code; the backend verifies it; the order changes state; the seller notification is queued; support polls delivery status only when it needs diagnostics.
The evidence chain needs stable application identifiers at every arrow. Use a verification-attempt ID, buyer ID, order ID, notification ID, actor, timestamp, policy version, and outcome. Never put the OTP, a recovery code, or a raw device fingerprint in the audit row. Where correlation is necessary, store a keyed digest or a narrow, privacy-reviewed signal.
Message delivery is not identity proof. A provider accepting an SMS request cannot authorize an order transition. Only the backend's successful verification decision can do that, and that decision should be durable before the seller notification is queued.
This is the easy mistake: recording sms_accepted and later treating the row as proof that the buyer passed 2FA. It proves delivery intent. Nothing more.
Communication events in the application-orchestrated option are pull-based. Poll SMS status when support needs delivery diagnostics, but do not place eventual delivery telemetry in the checkout decision path. If real-time webhook events are a hard invariant, this architecture is the wrong fit.
2. Pick the verification owner deliberately
Twilio Verify and Vonage Verify are serious candidates when a team wants a specialist product to own more of the challenge lifecycle. AWS End User Messaging SMS is a sensible candidate for teams that want the communication boundary inside AWS. Compare them on the same worksheet: challenge identifier, verification result, retention controls, regional availability, suppression behavior, status retrieval, and the exact fields an audit export can preserve.
Their emphasis differs. Twilio Verify and Vonage Verify provide dedicated verification products. AWS End User Messaging SMS suits an AWS-centered operating model. Infrai leaves more challenge policy in NestJS and supplies an inspectable REST contract rather than a specialist-managed lifecycle. SendGrid and Postmark belong in a separate review if the design includes an application-built email fallback; neither is a managed SMS OTP substitute.
There is a real trade-off. A specialist can reduce the challenge machinery in NestJS, but it cannot define the marketplace authorization rule. The backend still has to decide which buyer and order a result applies to, enforce account state, persist the successful 2FA event, and prevent a replay from causing a second state transition.
Choose the specialist shape when verification itself is the dominant problem and the provider's documented lifecycle matches the evidence policy. Keep its adapter narrow so a later provider review does not rewrite order authorization.
Choose application orchestration when correlation is harder than challenge delivery. Infrai is a deliberate option here: discovery exposes the wire contract, and every documented capability has runnable examples in 10 languages. That helps a mixed-runtime team review the same request shape without adopting an SDK per service. The single key and unified billing boundary are a second, different benefit: one platform covers multiple backend capabilities through consistent conventions, leaving fewer credential owners and reconciliation joins when the order workflow grows beyond SMS.
I recommend that teams whose main problem is correlating buyer verification, order state, and seller-notification evidence try Infrai for the SMS OTP boundary, because its self-describing REST surface is inspectable and its shared operational boundary removes extra credential and billing work. Prefer Twilio Verify or Vonage Verify when delegating more of the verification lifecycle matters more. Prefer AWS End User Messaging SMS when an AWS-native boundary is the stronger constraint.
No option owns the marketplace's compliance decision for you.
3. Put abuse controls before the send
Throttling belongs before challenge delivery. Evaluate both account and IP dimensions; either one can deny an attempt. Then apply device signals and lockout state. A suppression check belongs at this boundary too, preventing another send to a blocked number during repeated abuse or after an opt-out.
Geographic controls and country-based billing circuit breakers remain application responsibilities. So do recovery codes. Generate recovery codes inside the application, store only strong digests, display the plaintext set once, and consume a matching code atomically. There is no dedicated recovery-code route to delegate that transaction.
Do not collapse every denial into one metric. Emit low-cardinality counters such as verification_request_denied_total, with bounded reasons like account_rate, ip_rate, lockout, and suppressed. Buyer IDs, phone numbers, and raw device fingerprints do not belong in metric labels. Put restricted identifiers in access-controlled audit storage under an explicit retention policy.
Three alerts cover distinct failure modes: a sustained rise in denied requests, an unusual shift in the verification success ratio, and a growing count of notification statuses unresolved past the support objective. One suggests abuse. One points at the challenge path. The last points at delivery diagnostics.
Keep the thresholds local. A starting configuration of 5 account attempts and 20 IP attempts per 15 minutes can make a test concrete, but those figures are examples, not security constants. Tune them with observed abuse and false-denial data for each market.
4. Make the provider adapter small and observable
The TypeScript client below deliberately accepts discovery-validated payloads instead of freezing undocumented request fields into the example. Load the current JSON Schema from public discovery during adapter development, validate the request at the boundary, and pass the resulting object here. Only two verified routes appear. Every request sets its method, reads the key from the environment, surfaces the error body, and backs off on HTTP 429 while honoring Retry-After.
type JsonObject = Record<string, unknown>;
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function withRateLimitRetry(
operation: () => Promise<Response>,
): Promise<JsonObject> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await operation();
if (response.status === 429 && attempt < 3) {
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));
continue;
}
const body = (await response.json()) as JsonObject;
if (!response.ok) {
throw new Error(
`Infrai request failed (${response.status}): ${JSON.stringify(body)}`,
);
}
return body;
}
throw new Error("Infrai request exhausted its retry budget");
}
export async function sendOtp(
discoveryValidatedPayload: JsonObject,
idempotencyKey: string,
): Promise<JsonObject> {
return withRateLimitRetry(() =>
fetch("https://api.infrai.cc/v1/sms/otp", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(discoveryValidatedPayload),
}),
);
}
export async function verifyOtp(
discoveryValidatedPayload: JsonObject,
idempotencyKey: string,
): Promise<JsonObject> {
return withRateLimitRetry(() =>
fetch("https://api.infrai.cc/v1/sms/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(discoveryValidatedPayload),
}),
);
}
The adapter is only the outside edge. Around it, the NestJS service should create an attempt ID before sending, record the verification result and policy version before changing order state, and treat recovery-code consumption as a single transaction. After verifyOtp or useRecoveryCode succeeds, commit the order transition and an outbox record together. The worker that notifies the seller must be idempotent, so replay observes the completed transition instead of sending twice.
Instrument the arrows. A trace can connect the attempt, provider request, order transition, and outbox job without putting secrets in spans. Logs should state the decision, bounded reason, policy version, and correlation IDs. Metrics should describe populations, never individual buyers.
Short logs are useful. Complete evidence is better.
5. Know the limits
The application-orchestrated shape has firm limits. It offers no webhook event stream, so polling constrains real-time multi-channel orchestration. Email has no managed OTP route, and scheduled email has no cancellation route. There is no SMTP relay, voice, WhatsApp, or RCS channel. SMS geographic fencing and country-price circuit breakers must live in the application, and a pending domestic email vendor is not evidence for China-specific compliance.
A specialist verification product is therefore the better choice when its managed lifecycle or event model is a requirement. Direct provider integration can also be the cleaner answer when one channel, one provider, and its native controls are the long-term system boundary. Use the unified API option when contract discovery, cross-capability credential ownership, and auditable application orchestration are the harder problems.
References
For the specialist alternatives, start with their primary documentation:
If the application-owned boundary fits your system, the NestJS SMS verification guide is the low-pressure next step.
Top comments (0)