Short answer: for a cheap beginner OTP 2FA login architecture, make the challenge service the source of truth, use SMS as the normal delivery path with an explicit email recovery transition, and measure the evidence pipeline separately from delivery. In a logistics marketplace, a seller checking a new order needs that auditability more than a lowest per-message quote.
The first reliability rule is simple: a gateway accepting a message must never authenticate the seller. Authentication succeeds only when the challenge record validates the submitted code, its expiry, its attempt policy, and its one-time consumption. Delivery observations are evidence around that decision. They are not the decision.
Reliability failure modes in OTP delivery
The useful alarm is not “SMS failed.” It is an unexplained state. A reviewer should be able to tell which regional policy selected SMS, when the challenge was created, which template was rendered, when recovery became available, and why the code was consumed or expired. That record should contain a keyed account reference and event identifier, never the OTP, phone number, or email address.
In the seller workflow, the marketplace emits an order notification only after the authenticated session is established. A transport callback can move an event from delivery_requested to delivery_observed; it cannot move challenge_pending to challenge_verified. Keep those state machines apart. It makes an SMS outage a delivery incident instead of an identity incident.
A short message helps.
SMS encoding can change the reliability and cost model in ways that are easy to miss during a happy-path test. Twilio documents 160 characters for one GSM-7 segment and 70 for one UCS-2 segment, with smaller limits when a message is split. A localized curly quote or accented character can therefore create multiple segments. Test the rendered templates in every supported US and EU locale, and record the template version alongside the challenge event.
Email recovery has a different failure surface. Google's sender guidance requires authentication and describes additional expectations for bulk senders, so the fallback path needs its own domain configuration, reputation monitoring, and runbook. Treating email as “free backup” just hides a second unowned dependency.
How does a beginner build cheap OTP 2FA login architecture?
Use a single logical challenge with delivery attempts attached to it. A user request creates the challenge and records the policy version. A deliberate recovery action creates an email attempt under that same challenge; it should not race an SMS send or silently issue a second code. The event log then explains the sequence without implying that either channel proves possession.
Polling is a consumer concern. Persist a cursor and a stable ordering key, ingest events idempotently by event ID, and advance the cursor only after the evidence transaction commits. Re-reading a page, including an empty page, must be harmless. Set an evidence-freshness SLO from queue depth, expected challenge rate, receiving-system limits, and the review requirement; I'm not sure any fixed interval is correct without those measurements. A five-second interval might fit a small deployment, while a sixty-second interval could be acceptable for a slower audit view. Your mileage may vary, and that is exactly why the interval belongs in capacity planning rather than in a copied tutorial constant.
Here is the boundary I want in a Node.js service even though the example is Go: transport adapters report observations, while the challenge service alone verifies codes.
package otp
import (
"context"
"time"
)
type Channel string
const (
SMS Channel = "sms"
Email Channel = "email"
)
type DeliveryRequest struct {
ChallengeID string
DestinationRef string
Template string
Locale string
}
type DeliveryResult struct {
ProviderRef string
AcceptedAt time.Time
}
type Transport interface {
Send(context.Context, DeliveryRequest) (DeliveryResult, error)
}
type EvidenceEvent struct {
ID string
ChallengeID string
Kind string
Channel Channel
Region string
PolicyVersion string
TemplateVersion string
OccurredAt time.Time
}
type EventSink interface {
Append(context.Context, EvidenceEvent) error
}
type Service struct {
sms, email Transport
events EventSink
now func() time.Time
}
func (s *Service) RequestDelivery(ctx context.Context, channel Channel, req DeliveryRequest, event EvidenceEvent) (DeliveryResult, error) {
transport := s.sms
if channel == Email {
transport = s.email
}
result, err := transport.Send(ctx, req)
if err != nil {
return DeliveryResult{}, err
}
event.Kind = "delivery_observed"
event.Channel = channel
event.OccurredAt = s.now()
if err := s.events.Append(ctx, event); err != nil {
return DeliveryResult{}, err
}
return result, nil
}
The omitted pieces are deliberate responsibilities: cryptographically secure code generation, hashed-code storage, expiry and attempt checks, account and destination rate limits, atomic challenge updates, and an idempotency key for retries. In a Node.js implementation, the runtime changes but these contracts do not. Keep destination resolution inside the narrowest trust boundary, and redact values before they reach logs or evidence exports.
Data governance for regional evidence
US and EU routing should be a versioned policy, not a pile of environment variables. Store the selected region, channel, policy version, and template version on every transition. Retention and data placement depend on the marketplace's legal and contractual duties, so the runbook must name the owner who approves those settings rather than pretending one default fits every jurisdiction.
For capacity planning, estimate peak challenge creations per second, multiply by the maximum transitions per challenge, and reserve room for retries and event replays. Size short-lived challenge state separately from longer-lived evidence. Watch verification latency, delivery acceptance by channel and region, fallback rate, event-log depth, poller lag, duplicate-event rate, and terminal-state counts. One “OTP success” graph cannot represent both authentication and transport SLOs. I don't let a migration proceed until the on-call rotation can identify the policy and template versions for a single seller, replay the same event batch twice without changing the result, and explain what happens when the receiving system is at its rate limit; that exercise usually exposes an ownership gap long before production traffic does, because a nominally healthy gateway can still leave an evidence reader behind its retention window.
| Choice | Reliability upside | Operational trade-off |
|---|---|---|
| Managed SMS/email gateways | Carrier and mailbox operations stay outside the platform team | Provider observations must be normalized, and regional terms need review |
| Self-hosted delivery components | More control over data placement and internal interfaces | The team owns abuse controls, redundancy, carrier relationships, and on-call |
| Push-based event delivery | Lower audit-view latency when the source supports it | More connection state and replay handling |
| Pull-based polling | Simple recovery model with a durable cursor | Must budget API calls, lag, and retention headroom |
The catch is that SMS and email are unsuitable when policy forbids them as authentication factors or when sellers need offline, hardware-backed recovery. Choose an authenticator or hardware-backed method in that case, and keep these channels for notification. Stick with one channel plus a documented support process when a second automated path would exceed the team's review and incident capacity.
Evaluation tests for polling lag and replay
Test transitions, not screenshots. Cover successful SMS verification, explicit email recovery, expiry, repeated verification, duplicate event ingestion, cursor replay, an empty polling page, a locale that forces UCS-2, and a policy change between challenge creation and fallback. Assert that no event contains a destination or code.
Rollback changes assignments, not history. Stop issuing a suspect policy version, keep the previous version deployable, and let existing challenges finish under the version recorded at creation unless a security review requires invalidation. Record the rollback as a new event; never delete contradictory evidence. Roll back quickly.
Before release, ask an on-call engineer to answer three questions using only the evidence store: why one seller received SMS, why another was offered email recovery, and why a third challenge could not be reused. If the answer requires a gateway dashboard or personal data in application logs, the compliance SLO is already missed. Fix the record before tuning message rates.
References
- Google, “Email sender guidelines”: https://support.google.com/a/answer/81126
- Twilio, “SMS character limits and segmentation (GSM-7/UCS-2)”: https://www.twilio.com/docs/glossary/what-sms-character-limit
Top comments (0)