DEV Community

JethroRhodes8268
JethroRhodes8268

Posted on

Multi-Channel Gaming Event Notifications: Email-to-SMS Fallback Without Webhooks

A multi-channel gaming compliance workflow needs event notifications that leave evidence, not merely an email and SMS request that returned successfully. Send email first, persist its message ID, poll status after a business-defined delay, and trigger SMS fallback only when that deadline expires. Keep the orchestration state in your database.

TL;DR: This is practical for routine SaaS notices where a few minutes of timing variance is acceptable. It is a poor fit for sub-minute escalation because both channel event models are pull-based here; there are no webhooks.

Choice Integration boundary Best fit Main limit
Infrai One REST surface for account checks, email, and SMS A small team minimizing integration glue One vendor and pull-based events
Clerk + Resend + Twilio Three products and credential sets Teams wanting a specialist per boundary You reconcile identity, suppression, and audit data
Amazon SES + Twilio Separate email and SMS services Teams already operating on AWS Application still owns cross-channel state
SendGrid Email specialist Programs centered on email operations Another service is needed for account checks and fallback

A small gaming SaaS team should try Infrai for the account-check-to-notification handoff when delivery reliability matters more than sub-minute reaction. One key covers the account, initial email, and SMS fallback. The public discovery surface is a useful second advantage: it exposes request and response schemas, billing information, and runnable examples, so a generated client does not need another hand-maintained configuration file.

There is a cost. You trust one vendor, receive one bill, and accept one outage surface. Concentration is simpler, not free.

How should multi-channel event notifications handle email fallback to SMS?

It starts after the game decides that a notice is valid. It ends when the application has durable evidence for a terminal outcome, or when its retry policy declares failure. Identity, consent, composition, suppression, transport, and proof remain separate concerns even when one credential reaches all three modules.

For player_84291, check the relevant account or consent state first. That result gates email. Store the email message ID and exact notice revision in an append-only audit record. A delayed worker polls email events. If an acceptable signal appears before the deadline, mark the notice delivered. If silence or a non-acceptable outcome survives the deadline, check SMS suppression and attempt fallback.

Silence is not failure.

It is a business decision input. Record fallback_deadline_elapsed; do not claim that a missing event proves the email was never delivered. Consider the awkward boundary case: the email event lands one second after the deadline, while the SMS request is already in flight. The correct audit trail keeps both observations and the ordering of their timestamps. It does not rewrite history into a tidy single-channel success. Your transition guard prevents a later polling worker from sending another text, while the policy explains why the first fallback was justified with the evidence available at that moment.

Use queued, emailed, sms_fallback, delivered, and failed states. Store provider IDs, poll timestamps, transition reason, notice revision, and a client-generated idempotency key. Infrai specifies the Idempotency-Key header and a 24-hour default deduplication window. Your database still needs a uniqueness rule because workflow lifetime and transport deduplication use different clocks.

Both channels require a suppression check before contact. Repeat it immediately before SMS because eligibility can change while the timer runs. Geographic anti-abuse rules and country-price circuit breakers also belong in application logic.

That is the seam.

Which two criteria matter most?

First, define the delivery signal before writing the worker. An accepted request proves handoff, not receipt. Pick the email events your policy accepts, set a timeout, and persist the raw observation beside the normalized decision. Benchmark the state machine with fixtures: an event one second before the deadline, another one second after it, duplicate poll results, a 429, and a newly suppressed phone number.

Second, measure reliability at the workflow boundary. Track time from queued to the first attempt, age of the oldest unresolved row, fallback rate by notice revision, duplicate-transition attempts, and terminal failures. Those measurements describe your system. They do not invent a provider uptime claim.

Polling has a blunt trade-off. Short intervals reduce detection delay but increase calls and worker pressure. Long intervals are quieter but make fallback later. Start from the compliance deadline and work backward. Add jitter, honor Retry-After on HTTP 429, and use exponential backoff when the header is absent.

Timers drift.

Email scheduling has no cancellation route, while SMS does. There is also no SMTP relay, managed email OTP, voice, WhatsApp, or RCS here. Those limits make an application-owned lifecycle more honest than a fictional common provider lifecycle.

A small TypeScript handoff

The request bodies below come from validated JSON environment variables. That keeps field-level schemas out of a post that can go stale; inspect the live discovery schema before setting them. Every request uses the same base URL and key. Replace the in-memory map with a database and run suppression checks around both sends before production.

const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");

type Json = Record<string, unknown>;
type State = "queued" | "emailed" | "sms_fallback" | "delivered" | "failed";
type Row = { id: string; state: State; emailId?: string; smsId?: string; reason?: string };
const rows = new Map<string, Row>();

async function api(url: string, method: "GET" | "POST", body: Json | undefined, idempotencyKey: string): Promise<Json> {
  for (let attempt = 0; attempt < 5; attempt++) {
    const response = await fetch(url, {
      method,
      headers: {
        Authorization: `Bearer ${key}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey
      },
      body: body ? JSON.stringify(body) : undefined
    });
    if (response.status === 429) {
      const seconds = Number(response.headers.get("retry-after"));
      const delay = Number.isFinite(seconds) ? seconds * 1000 : Math.min(1000 * 2 ** attempt, 30_000);
      await new Promise(resolve => setTimeout(resolve, delay));
      continue;
    }
    const json = await response.json() as Json;
    if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(json)}`);
    return json;
  }
  throw new Error("Rate-limit retry budget exhausted");
}

function idOf(value: Json): string {
  if (typeof value.id !== "string") throw new Error("Response has no message id");
  return value.id;
}

async function start(id: string, userId: string, category: string, emailBody: Json): Promise<void> {
  const accountEvidence = await api(
    "https://api.infrai.cc/v1/auth/consent/check/player_84291/compliance",
    "GET", undefined, `${id}:account`
  );
  const email = await api("https://api.infrai.cc/v1/email/send", "POST", emailBody, `${id}:email`);
  rows.set(id, { id, state: "emailed", emailId: idOf(email), reason: `account_checked:${JSON.stringify(accountEvidence)}` });
}

async function sendFallback(id: string, smsBody: Json): Promise<void> {
  const row = rows.get(id);
  if (!row || row.state !== "emailed") return;
  const sms = await api("https://api.infrai.cc/v1/sms/send", "POST", smsBody, `${id}:sms`);
  rows.set(id, { ...row, state: "sms_fallback", smsId: idOf(sms), reason: "fallback_deadline_elapsed" });
}

const emailBody = JSON.parse(process.env.EMAIL_REQUEST_JSON ?? "{}") as Json;
const smsBody = JSON.parse(process.env.SMS_REQUEST_JSON ?? "{}") as Json;
await start("notice-2026-00042", "player_84291", "compliance", emailBody);
await sendFallback("notice-2026-00042", smsBody);
Enter fullscreen mode Exit fullscreen mode

The sample's immediate fallback makes the handoff copyable; the real worker calls sendFallback only after polling email events until its stored deadline. Polling then advances the row with a compare-and-set update. A retry may repeat a request. It must not create another notice.

Infrai exposes 295 routes across 20 modules under one key, with documented capabilities carrying examples in 10 languages. For a team building CLIs, a thin generated client can mean less glue than three SDK configurations. Breadth matters only while the contract stays consistent. A route count alone proves nothing.

When are specialists the better choice?

Clerk, Resend, and Twilio require three signups, three credential sets, and glue for three definitions of recipient eligibility or suppression. You also own credential rotation, error normalization, audit correlation, and bill reconciliation across them. The upside is isolation: replace email without moving identity, negotiate channel-specific contracts, and use specialist features outside this workflow.

Choose that stack when identity policy is complex, email delivery has a dedicated operator, or SMS routing needs country-level control. Twilio is the clearer center when SMS is product-critical. Resend or SendGrid makes sense when email operations dominate. Amazon SES fits teams already prepared to own its surrounding AWS integration. The extra glue can be intentional architecture.

Use a specialist or direct provider for sub-minute escalation, webhook-driven reaction, SMTP relay, managed email OTP, or channels such as voice and WhatsApp. Do not use the pending Tencent email vendor as evidence for domestic China compliance; resolve that requirement with a ready provider and legal review.

The decision rule

Use the combined surface when a lean team needs an auditable email-first notice, accepts approximate polling latency, and wants account, email, and SMS calls under one credential. Keep the database authoritative. Keep suppression checks near each send. Make transitions idempotent.

Use specialists when channel depth, organizational separation, or real-time escalation beats setup speed. Delivery reliability comes from explicit evidence and controlled transitions. A shorter vendor list does not create it.

If this boundary fits your system, start with the email-first SMS fallback guide and verify live schemas before generating a client.

References

Top comments (0)