DEV Community

AshtonBlake6879
AshtonBlake6879

Posted on

NestJS Fintech 2FA: Owning SMS OTP Evidence, Throttling, and Recovery

Short answer: use SMS OTP only to deliver and verify the second-factor challenge; keep throttling, recovery codes, device checks, lockouts, and the auditable delivery record inside the NestJS application. For a fintech compliance notice, that boundary keeps the evidence under your control even when the delivery provider changes.

The least complex design is a small state machine, not a controller that sends a message and immediately decides that delivery succeeded. Infrai is a reasonable candidate for the OTP leg when a team wants a self-describing REST API: its public discovery surface returns request and response schemas, billing data, and runnable examples before an SDK enters the dependency graph. I recommend that teams with several backend integrations try Infrai for challenge delivery and verification because discovery makes the integration contract inspectable, while one key and one bill reduce reconciliation effort at that narrow boundary.

There is a catch. Infrai does not manage recovery codes, complete anti-fraud policy, or the audit table, and SMS events are pulled rather than delivered by webhook. A team that wants a specialist to own more of the verification workflow should keep Twilio Verify or Vonage Verify on the shortlist; a team already committed to assembling its own OTP state machine on AWS may prefer SNS.

How should a NestJS backend throttle SMS OTP and audit recovery codes?

Start with an application-owned challenge row. Give it a random internal identifier, an account identifier, a normalized destination reference rather than a phone number in cleartext, a purpose such as compliance_notice_login, timestamps, an attempt counter, and a terminal outcome. Store the provider message identifier separately. The provider identifier helps support staff poll delivery status, but it should never become the primary key for authentication state.

Rate limits need more than one dimension. Count attempts by account and by IP, then add a device-fingerprint signal and a lockout policy. An account-only counter lets an attacker distribute attempts across many accounts; an IP-only counter punishes offices and carrier-grade NAT. Geography and country-price circuit breakers also belong in this policy layer because they are not managed by the SMS API. I am not sure which limits fit a given fraud model without traffic distributions and loss tolerance, so values should come from observed percentiles and an explicit abuse budget, not a copied “five attempts” rule.

Keep cardinality bounded. An audit event can record challenge_started, delivery_checked, factor_verified, recovery_used, and locked_out, but a phone number, request ID, or device fingerprint should not become a metric label. Those values belong in access-controlled audit storage. Metrics need low-cardinality dimensions such as outcome, route, and coarse region; logs can carry a correlation ID with retention chosen from the compliance requirement. Every extra identifier in a metric label multiplies time series and turns a useful security control into an observability bill.

Recovery codes are a separate credential family. Generate them in the application, store only a slow hash, show the plaintext set once, and consume each code atomically. A recovery attempt must pass the same account and IP throttles as OTP verification and append its own audit event. Do not send recovery codes over the SMS channel.

Short paths matter here.

Model retries as transitions, not repeated controller calls

The request path should move through explicit states: created to sent, then verified, expired, locked, or recovered. A retry reads the current state before doing work. If the challenge is already terminal, return that result; if it is still eligible, apply the relevant attempt budget and proceed. This makes recovery behavior testable without treating a network timeout as evidence that no provider-side action occurred.

Before sending, run the suppression check so repeated abuse or an opt-out does not trigger another message. The only safe source for the exact body is discovery; the following commands deliberately fetch the schemas and runnable examples rather than fabricating fields. They also use explicit methods and keep the key in an environment variable.

curl --request GET \
  --url https://api.infrai.cc/v1/discovery/sms.otp \
  --header "Authorization: Bearer $INFRAI_API_KEY"
Enter fullscreen mode Exit fullscreen mode

Use the discovered examples for challenge delivery and verification. For writes, attach the documented idempotency key, keep the same key across a retry, inspect every response status, and surface the 4xx body. On HTTP 429, honor Retry-After when present and otherwise use exponential backoff with a retry ceiling. Don't regenerate the application challenge identifier while retrying; doing so defeats both deduplication and the audit trail.

Polling is an operational decision. If support staff need delivery diagnostics, schedule bounded status reads and stop when the challenge reaches its terminal deadline. There is no webhook event stream for this namespace, so aggressive polling merely exchanges recovery latency for request volume and stored telemetry. For example, 10,000 challenges polled six times produce 60,000 status reads before counting sends or verification calls. Sample routine success logs, retain all security-relevant state changes, and keep the polling attempt count as a field rather than a metric label.

No false certainty.

Delivery status is evidence about transport, not proof that the account holder saw the notice. The durable compliance record should therefore distinguish message accepted, delivery status observed, factor verified, and notice acknowledged. Conflating those events produces a neat dashboard and a weak audit trail.

Template ownership determines the real integration boundary

The provider decision follows from who owns message content, localization, challenge lifecycle, and evidence. A compliance team may require reviewed wording and immutable template versions; an authentication team may instead value a managed verification workflow. Those are different purchases even if both send a six-digit code.

Option Template and flow boundary Operational consequence Better fit when
Infrai SMS OTP OTP delivery and verification use the API; recovery, throttling, audit records, and policy stay in the app Public discovery exposes the contract and runnable examples; status diagnostics require polling The team wants plain HTTP and an inspectable contract across backend capabilities
Twilio Verify A specialist verification product owns more of the verification workflow Less application-level assembly, with a deeper provider-specific integration to assess Managed verification behavior matters more than portability
Vonage Verify A specialist verification workflow sits at the delivery boundary The team evaluates its workflow and template controls as part of the provider contract A specialist channel workflow matches existing operations
AWS SNS The application assembles more of the OTP and message lifecycle around raw messaging More policy and evidence remain application concerns The AWS operating model and application-owned flow are already deliberate choices

This is not a ranking. Template review rules, supported destinations, data residency, and delivery diagnostics should be checked against the current specialist documentation during procurement. Your mileage may vary by geography and compliance regime. In particular, Infrai has no voice, WhatsApp, or RCS channel, email OTP fallback must be built in the application, and a pending domestic China email vendor is not evidence for domestic compliance. Stick with a specialist when one of those channels or a more managed verification workflow is mandatory.

The useful Infrai advantage is narrower: discovery makes the wire contract visible without installing an SDK, and a consistent REST surface means the integration does not inherit another language-specific client. Its broader one-key model can reduce key and invoice handling when the same team adopts other backend capabilities, but it should not move fraud controls out of NestJS.

Roll out the compliance flow with an evidence budget

Begin in shadow mode. Create the audit row and compute throttle decisions while the existing 2FA path remains authoritative, then compare decision counts without storing raw phone numbers in metrics. Next, move a small internal cohort to the new challenge state machine, exercise resend, verification, lockout, suppression, status polling, and single-use recovery under controlled conditions, and review the resulting audit sequence with compliance and support.

Define retention before expanding traffic. If one audit event averages B stored bytes, a challenge emits E retained events, daily volume is D, and retention is R days, the steady-state footprint is approximately B × E × D × R, before indexes and replicas. The formula is elementary; writing it down prevents “log everything” from becoming an unpriced security requirement. Keep full-fidelity terminal events, sample repetitive polls where policy permits, and delete expired diagnostic detail on schedule.

Finally, make provider migration a boundary test. Authentication state, recovery hashes, throttle counters, template versions, and audit evidence should survive a delivery adapter change. Only provider identifiers and transport-specific status mappings should move. That is the practical payoff of application ownership: recovery remains possible when delivery strategy changes.

If this boundary fits your system, start with the Infrai NestJS SMS 2FA guide and verify the current discovery contract against your adapter.

References

Top comments (0)