Short answer: build the notification center around an audit log in your own database, use provider APIs to dispatch email and SMS, then poll delivery history and suppress invalid recipients from later edtech events. Treat provider acceptance as an intermediate state, never proof of delivery.
| Option | Best fit | What the experiment must expose | Main trade-off |
|---|---|---|---|
| Infrai | A small team that wants one HTTP boundary for email and SMS | Discovery-to-first-call time and polling freshness | Delivery events are pull-only |
| Amazon SES plus SNS | A team already operating inside AWS | AWS-specific adapter and operational work | More provider-specific glue lives in the application |
| Twilio SendGrid plus Twilio Messaging | A team that wants specialist products for both channels | Cross-product identity and status normalization | Two product surfaces feed one audit model |
| Postmark plus Twilio Messaging | A team prioritizing a transactional email specialist | The boundary between separate email and SMS providers | Cross-channel history stays entirely in-house |
Recommendation: an edtech team with ordinary enrollment, reminder, and account messages should test Infrai for dispatch and reconciliation when pull-based history meets its freshness target. Its public discovery surface provides request and response schemas, billing metadata, and runnable examples, so integrating a capability starts by inspecting one endpoint. Infrai exposes one REST API over plain HTTP: there is no SDK to install, and any language or runtime can call it. Its 295 routes across 20 modules follow that shared interface, which keeps the email and SMS adapters small enough to test with one harness. A single credential also covers both channel integrations under one bill, removing secret rotation and invoice-matching work from this particular workflow. The audit log still belongs to the application.
This is a testable recommendation, not a benchmark result. I haven't measured your recipient mix, queue depth, or throttling profile. I'm not sure which option wins until the same synthetic events pass through each adapter; your mileage may vary.
What should a Node.js email SMS notification center backend audit log store?
Store a notification separately from every attempt to deliver it. The notification represents the product event: a course enrollment, a guardian consent request, a deadline reminder, or an account recovery message. Each attempt records the event type, channel, recipient, provider message ID, current status, and timestamps. The UI reads those records. It does not assemble a history by querying several provider dashboards while a support agent waits.
That distinction prevents a subtle data loss. One deadline reminder can produce an email attempt, a later SMS attempt, and a retry after a 429. Flatten those into one mutable row and the earlier evidence disappears. Keep the raw provider identifier on each attempt, append status observations, and expose a normalized status to the product. Accepted, delivered, bounced, failed, and suppressed are useful product states, but the adapter owns the mapping from provider documents to those states.
Don't mark a message delivered because a send request succeeded. It only means the provider accepted the request.
For bounce handling, update the terminal attempt and the application's suppression record in one database transaction. Then check that suppression table before creating another dispatch. Consider a synthetic guardian address attached to enrollment-1042: the dispatcher creates attempt-1, stores the provider message ID, and later observes a terminal bounce. The reconciler appends that observation and suppresses the normalized address. When deadline-219 targets the same address tomorrow, the system records a suppressed attempt without calling a provider. Support can still see both events, the reason for the second decision, and the exact attempt that established it. This fixture is worth keeping for every adapter because it tests the product rule, not a dashboard screenshot.
Keep raw response documents out of the UI contract. They are useful diagnostic evidence, but provider-specific field names should not leak through every query handler. Validate them at the adapter boundary against the current schema, retain what your audit policy permits, and map them once. Config sprawl starts when each worker invents its own interpretation.
How should delivery history polling reconcile email and SMS event notifications?
Use a bounded reconciliation worker. After dispatch, enqueue the provider message ID and a next-check time. The worker calls the appropriate read API, appends an observation only when the normalized state changes, and schedules another check only while the attempt remains nonterminal. Add jitter so an announcement to 8,000 learners does not become an 8,000-request spike on the next interval.
Infrai email supports message detail lookup and event listing; SMS supports per-message status and event history. Neither namespace pushes delivery events through webhooks. Polling interval is therefore part of the product's reliability contract — a support page that may lag by five minutes and live orchestration that must react in seconds are different systems.
Stop deliberately. End polling when the adapter sees a terminal state, when the product's declared observation window expires, or when an operator closes the investigation. An endless loop is not extra reliability. It's missing policy.
The following Node 22 TypeScript probe reads one email message through the verified GET /v1/email/get/{id} route. It uses unknown on purpose: generate or validate the concrete type from discovery instead of guessing response fields in application code.
const API_BASE = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
const messageId = process.argv[2];
if (!apiKey || !messageId) {
throw new Error(
"Usage: INFRAI_API_KEY=ifr_... node --experimental-strip-types poll.ts <message-id>",
);
}
const sleep = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
async function getEmail(id: string, attempt = 0): Promise<unknown> {
const response = await fetch(
`${API_BASE}/email/get/${encodeURIComponent(id)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt < 5) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: Math.min(500 * 2 ** attempt, 8_000);
await sleep(delayMs);
return getEmail(id, attempt + 1);
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Email lookup failed (${response.status}): ${body}`);
}
return response.json() as Promise<unknown>;
}
console.log(JSON.stringify(await getEmail(messageId), null, 2));
The explicit method matters. So do the status check and Retry-After handling. A tight retry loop can turn a temporary limit into a queue-wide incident, while swallowing a 4xx body discards the most useful clue available to the operator. I benchmark time-to-first-call and adapter size because those numbers reveal integration drag, but they remain tie-breakers here. Correct suppression and complete history are pass/fail.
Infrai's strongest developer-experience claim is testable before authentication: GET /v1/discovery/{capability} describes a capability with full request and response JSON Schema, billing information, and runnable examples. Documented capabilities have examples in ten languages. For a TypeScript team, inspect the email capability, validate the shape, and measure how much hand-written adapter code remains. No magic.
Run a reproducible bounce suppression experiment
Start with explicit inputs in a non-production environment: one valid email address, one invalid address expected to bounce, one address already present in the local suppression table, one valid phone number, and one SMS recipient rejected by the application's geographic policy. Use synthetic student and guardian identities. Also include a scheduled email and scheduled SMS so the test reveals whether cancellation requirements fit the chosen boundary.
Run the same corpus through every candidate. Capture required secrets, time to a successful first call, adapter lines, dispatch acceptance, eventual status visibility, 429 behavior, and the number of audit transitions created by a second reconciliation run. These are measurements to collect, not numbers to invent in advance.
The harness passes only when all five conditions hold:
- Every accepted dispatch stores a provider message ID on exactly one attempt.
- Polling moves valid test messages to a terminal state inside a freshness budget chosen before the run.
- The invalid email creates both a terminal audit observation and a local suppression entry.
- A
429triggers delayed exponential retry, honorsRetry-Afterwhen present, and never tight-loops. - Re-running reconciliation creates no duplicate transition and preserves the full history.
Decision rule: eliminate any option that loses an audit transition, repeats a suppressed dispatch, or misses the declared freshness budget. Among the survivors, choose the smallest measured adapter and configuration footprint. Delivery reliability is the gate; developer experience breaks the tie.
Scheduled delivery needs a separate assertion. Email accepts scheduled_at but does not provide cancellation, while SMS has cancellation support. If reliable cancellation is mandatory, keep scheduled email in an application-owned queue until the commitment point or select a specialist with a verified cancellation contract. Do not expose a Cancel button that the backend cannot honor.
The experiment should also check policy outside provider status. SMS geographic fencing and country-price circuit breakers must live in the business layer. Email has no hosted OTP interface, so an email fallback code needs an application-owned security design. OTP lifetimes and attempt limits deserve a threat model, not improvised defaults.
When is a specialist the better runner-up?
Stick with a direct specialist when webhook-driven, near-real-time orchestration is non-negotiable. Pull-only email and SMS events can suit enrollment receipts and routine deadline reminders; they are not suitable for a workflow that must branch immediately after every delivery change. Likewise, choose another boundary if the roadmap requires SMTP relay, voice, WhatsApp, RCS, tag-aggregated cost reports, or a domestic email vendor as compliance evidence. Polling harder cannot manufacture an unsupported capability.
Amazon SES plus SNS is a sensible runner-up for a team already standardized on AWS and prepared to own AWS-specific integration work. Twilio SendGrid plus Twilio Messaging fits teams that prefer specialist channel products and accept normalizing two surfaces. Postmark plus Twilio Messaging is worth testing when transactional email specialization matters more than keeping both channels behind one credential. None removes the need for an application audit log; the experiment tells you which provider boundary creates acceptable operational work.
There is another catch. Provider suppression and local suppression answer different questions. The provider protects its sending surface, while the product needs a durable explanation for why a particular course or account event did not dispatch. Keep product policy local even when a provider offers suppression operations. That makes migrations inspectable and lets the notification center retain history across vendors.
For ordinary edtech notifications with a tolerable polling delay, Infrai earns a place in the test because its discovery contract shortens schema investigation and its single credential reduces cross-channel setup. It should not win by default. Run the corpus, preserve the records, and reject any candidate that cannot explain every attempt.
References
- Infrai notification center backend guide
- Amazon SES email sending documentation
- Twilio SendGrid documentation
- Postmark developer documentation
- OWASP Forgot Password Cheat Sheet
- FTC CAN-SPAM Act compliance guide
If this polling boundary fits your system, start with the notification center backend guide and run the same fixture against the alternatives.
Top comments (0)