DEV Community

TitanJ53
TitanJ53

Posted on

Node.js Urgent Event Notifications for Gaming: SMS First, Email Fallback, Polling

Short answer: treat the contact form as a durable event, route it to a support queue before sending anything, try SMS for the urgent path, poll for a bounded delivery result, and enqueue one email fallback when the SMS is terminally undelivered or misses the event deadline. The queue owns the workflow; a provider response does not.

This is an architecture decision record for a gaming support backend. A player might report a payment lockout, an account takeover warning, or a live-service incident through a contact form. The important result is not “a request returned 200.” It is a traceable support event that reaches the right people without producing duplicate pages, duplicate emails, or an unbounded retry loop.

The support event and its audit record

The form handler should validate the submission, assign an event ID, classify its urgency, and write a queue record in one durable transaction boundary. It should then return a receipt to the player. Sending SMS from the request handler couples the player-facing latency to carrier behavior and makes a process restart difficult to reason about.

The event needs a stable identity. A channel attempt needs its own identity. Those are different things: one support event can have one SMS attempt and one email fallback, while a retry of a throttled HTTP request must reuse the same operation identity.

For this scenario, I would record at least:

  • event_id, queue name, urgency, and received timestamp
  • recipient reference and the country policy decision (US or EU)
  • channel state, provider message ID, next poll time, and hard deadline
  • consent or lawful-basis reference, suppression result, and content template version
  • fallback deduplication key, final outcome, and the timestamps of every transition

The queue is the source of workflow truth. A delivery status is evidence about one attempt, not permission to create another one.

Queue first.

How should a Node.js state machine handle urgent SMS and email fallback?

Use two clocks. The transport clock handles a rejected API call, such as a rate limit, with bounded backoff. The delivery clock waits between status checks. They must not share a sleep loop: an HTTP retry is trying to complete one operation, while polling is asking what happened to an operation that already completed its submission.

The critical path below is deliberately provider-neutral. send_sms, poll_sms, and enqueue_email are application adapters with contracts that your selected channel services must satisfy. The state machine is the part worth testing. It does not pretend that a generic example can certify carrier-specific delivery states.

from dataclasses import dataclass
from enum import Enum
from typing import Callable, Optional


class SmsState(str, Enum):
    PENDING = "pending"
    DELIVERED = "delivered"
    UNDELIVERED = "undelivered"
    SUPPRESSED = "suppressed"


@dataclass
class Notification:
    event_id: str
    support_queue: str
    recipient: str
    country: str
    deadline_at: float
    sms_id: Optional[str] = None
    state: SmsState = SmsState.PENDING
    email_enqueued: bool = False


def process_notification(
    record: Notification,
    now: Callable[[], float],
    send_sms: Callable[[str, str], str],
    poll_sms: Callable[[str], SmsState],
    enqueue_email: Callable[[str, str, str], None],
    wait: Callable[[float], None],
) -> str:
    if record.country not in {"US", "EU"}:
        return "policy-rejected"

    if record.sms_id is None:
        record.sms_id = send_sms(record.event_id, record.recipient)

    while record.state == SmsState.PENDING and now() < record.deadline_at:
        wait(5.0)
        record.state = poll_sms(record.sms_id)

    if record.state == SmsState.DELIVERED:
        return "sms-delivered"

    if not record.email_enqueued:
        enqueue_email(
            record.event_id,
            record.recipient,
            f"support-fallback:{record.event_id}",
        )
        record.email_enqueued = True
    return "email-enqueued"
Enter fullscreen mode Exit fullscreen mode

In real code, the record update and the unique fallback insert need transactional protection or an equivalent compare-and-set operation. A worker can die after inserting the email job and before acknowledging the queue item; on redelivery, the deduplication key must make that replay harmless. The example keeps those persistence calls outside the adapter signatures so the invariant stays visible.

There is a subtle deadline race here. Imagine a player submits a payment-lockout form at 10:00:00, the SMS is accepted at 10:00:01, and the worker's final poll at 10:00:45 still says pending. The worker selects email fallback, but the carrier marks the text delivered at 10:00:46. If the code treats that late status as a reason to roll back the queue decision, two workers can disagree about whether the email should exist; if it treats the late status as a fresh failure, support loses the useful fact that the SMS arrived. Persist the escalation decision once, then record both outcomes against their separate attempt IDs. That is why the audit model must allow both channel outcomes. A late delivery updates the SMS attempt; it does not create a second email or erase the fact that escalation was already selected.

Five seconds is an example interval, not a universal SLA. Carrier behavior, alert severity, queue age, and the player's region should drive the value. I'm not sure a single deadline is defensible for every game and every incident class; measure the actual decision lag and make the policy configurable. Your mileage may vary.

Choose the boundary the team can operate

The decision is about failure ownership. A direct SMS adapter plus a separate email adapter gives the application explicit control over the state machine, but the team owns identity correlation, throttling, suppression handling, and reconciliation. A managed workflow can reduce integration work, but it may constrain escalation timing, audit fields, or country-specific policy. An in-house mail relay gives control over delivery evidence and reputation, while creating a larger operations burden.

Approach What the application must own Where it fits Reconsider it when
Separate channel adapters Correlation, fallback uniqueness, polling, policy, and audit A team that needs channel-specific controls The team cannot operate a durable worker or reconcile late results
Managed escalation workflow Policy integration, event identity, and acceptance tests A team with a verified workflow contract and limited messaging operations The workflow cannot expose the states or timing needed by support
Self-hosted mail delivery with SMS adapter Mail reputation, queueing, suppression, and SMS correlation A team with established mail operations The team needs a small operational footprint or lacks delivery expertise

The catch is that no option removes consent, suppression, sender registration, quiet-hours policy, or data-processing review. US and EU recipients should be handled by explicit policy, not by a country guess buried in a formatter. For an urgent support notification, send only the minimum incident detail and link to an authenticated support record; putting account secrets or payment data into a text message is a bad trade.

Email also has a standards obligation that is easy to forget during an urgent flow. Marketing and subscription mail need an unsubscribe path, and RFC 8058 describes the one-click mechanism for List-Unsubscribe. A transactional support escalation is a different category, but the classification should be recorded and reviewed rather than assumed from the subject line.

What does a 03:00 failure test reveal?

The happy path is a weak test. For every event, assert that a duplicate worker claim cannot issue a second SMS. Simulate a timeout after submission and verify that the persisted SMS ID, rather than a new send, is used on recovery. Return a pending status through the deadline, then confirm one email job. Return a late delivered status after fallback and confirm that the final audit record contains both outcomes without another enqueue.

Rate limits deserve their own tests. Honor a server-provided retry delay when the transport call is throttled, cap the number of attempts, and add jitter so a fleet does not wake on the same second. A 429 is not evidence that the carrier failed to deliver the message; it is evidence that this particular API operation needs later attention.

Then test the unglamorous parts: an invalid destination, a suppressed address, an expired queue lease, a worker restart during the database transaction, and two processes racing on the same event. Observe queue age, poll lag, fallback count, duplicate-prevention conflicts, terminal states by country, and the fraction of alerts that remain unresolved at the hard deadline. Those metrics tell support whether “SMS first” is helping or just hiding missing delivery evidence.

Keep logs free of message bodies and one-time codes. Correlate records with the event ID and channel-attempt ID, and retain the provider's status payload only when the data policy permits it. A support team needs to know which queue owns the event and why escalation happened; it does not need a copied secret in a log search.

US and EU policy boundaries

This design is not suitable when the organization cannot guarantee durable queue processing, idempotent storage, and an on-call owner for delivery reconciliation. In that case, use a simpler single-channel support workflow or select a managed workflow only after it demonstrates the required country controls, state visibility, and deadline behavior in a staging test.

Stick with separate adapters when the game already has reliable channel integrations or needs specialist controls. Revisit the architecture when the business adds voice escalation, richer regional policy, or a requirement for provider-originated events with a stricter reaction time. The deciding evidence should be observed delivery and reconciliation behavior, not a feature checklist.

The backend should promise a durable attempt and a clear audit trail. It should not promise that one transport will always win.

Further reading

Top comments (0)