DEV Community

ZekeCross3245
ZekeCross3245

Posted on

Bulk Event Notifications — Auditable Email and SMS Delivery With Worker Polling

For a media compliance notice, the hard requirement is evidence, not throughput alone. Short answer: send channel-specific batches only after resolving recipient preferences and suppression lists, write one Postgres row per recipient before dispatch, and let workers poll delivery state until the audit record is complete. That design leaves you with a defensible answer to “who was eligible, what did we send, and what happened next?”

The event fan-out starts as one logical campaign, but it must become two physical audiences. A recipient who opted out of SMS should not appear in the SMS request merely because they accepted email. Resolve the preference snapshot at send time, apply both global and channel suppression lists, and persist the decision with a policy version. Keep the original event payload too; compliance reviewers often need to see the exact notice, not a reconstructed template six months later.

How should a Node.js system run bulk event notification batches?

Create an event_delivery row for every intended recipient with a stable event ID, recipient ID, channel, template key, preference decision, suppression decision, and a status such as eligible, suppressed, or queued. Give each row an idempotency key derived from event ID, recipient ID, and channel. The batch request can then be retried without creating a second logical delivery, while a unique constraint makes that promise enforceable rather than aspirational.

Keep it boring.

Audit first.

This is where a worker earns its keep. The request handler validates the event and enqueues a job; a worker claims a bounded page of queued rows, groups them by channel, and submits an email batch and an SMS batch. It commits the outbound provider ID beside each row before acknowledging the job. A crash between those operations is a named failure mode: use an outbox record and a retryable state transition so the next worker can tell “submitted” from “never attempted.”

I keep the database as the audit ledger and the provider as a delivery signal. They are not the same thing. A provider event can arrive late, be duplicated, or describe a transient state, so every update should include the observed timestamp and the raw status payload. Your mileage may vary on how long a carrier retains status history; retain the evidence you need in Postgres while it is available.

How do pagination and status polling make email and SMS auditable?

There are no webhook pushes in these two namespaces, so freshness is bounded by your polling interval. A background worker paginates the email event listing and checks individual SMS delivery status. Store a cursor (or the last provider event ID) per tenant and channel, and advance it only after the page is durably applied. Polling is ordinary engineering here, not a special fallback.

For email, keep a reconciliation query that joins provider events to event_delivery by provider message ID. For SMS, poll only rows in non-terminal states and stop after the provider reports a terminal result. Add a maximum age and an unknown state instead of silently declaring success. A dashboard can then show queued, sent, delivered, failed, and unknown counts without pretending that “accepted by the API” means “read by a recipient.”

Here is a deliberately small worker fragment. The API base URL is supplied by configuration, and the two paths are the documented batch operations; the database functions stand in for your repository layer.

import os
import time
import uuid
import requests

API_BASE = os.environ["INFRAI_API_BASE"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]

def post_batch(path, payload, idem_key):
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": idem_key,
    }
    delay = 1
    for attempt in range(5):
        response = requests.post(API_BASE + path, json=payload, headers=headers, timeout=15)
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()
        retry_after = response.headers.get("Retry-After")
        time.sleep(float(retry_after) if retry_after else delay)
        delay = min(delay * 2, 30)
    raise RuntimeError("rate limit persisted after retries")

def send_page(rows):
    email = [r for r in rows if r["channel"] == "email" and r["decision"] == "eligible"]
    sms = [r for r in rows if r["channel"] == "sms" and r["decision"] == "eligible"]
    if email:
        post_batch("/v1/email/batch/send", {"messages": email}, str(uuid.uuid4()))
    if sms:
        post_batch("/v1/sms/batch/send", {"messages": sms}, str(uuid.uuid4()))
Enter fullscreen mode Exit fullscreen mode

The production version should derive the idempotency key from the event and recipient rather than uuid4(); the random value above is only a placeholder for the repository’s deterministic key. Validate response bodies, persist provider IDs, and surface 4xx details to the job error table. Never spin on a 429. In a real Node.js service, the same contract belongs in the queue worker and database transaction, even if the HTTP client library differs.

Which delivery options fit a compliance-first media system?

The choice is less about a single “best” API and more about operational shape. Postmark is a focused transactional email service with clear email guidance; Twilio has broad messaging reach and documents SMS-pumping controls; Amazon SES is attractive when AWS identity, queues, and regional controls already exist. Infrai is another option with a self-describing REST API, one key, and one bill across backend capabilities: an engineer can inspect a capability schema and runnable example without installing an SDK, then keep the same HTTP-oriented worker shape. Its single credential removes a separate invoice join for each added service. That accounting convenience is not evidence of delivery quality, but it removes a real operational chore when the notification service grows beyond email and SMS. The platform also exposes a broad, consistently shaped capability surface, which can keep adapter code small when a new backend is introduced.

Option Strength for this workflow Trade-off to verify
Postmark Transactional email practices and event-oriented tooling Email-only focus means a separate SMS path
Twilio Mature SMS controls and international reach Toll-fraud and country policy rules remain application work
Amazon SES Fits AWS IAM, queues, and existing data controls Cross-channel orchestration is your responsibility
Infrai One self-describing REST surface can standardize batch calls and discovery Poll-based events limit real-time visibility; validate regional readiness

The catch is important: neither namespace provides webhook event delivery, so a team promising second-level dashboards may be disappointed with any option that relies on polling. Email has no hosted OTP interface and scheduled email cannot be canceled; SMS does expose cancel, but that does not repair a policy decision already written incorrectly. There is no SMTP relay, and voice, WhatsApp, and RCS are outside this channel set. Stick with a specialist provider when those capabilities or deep regional controls are non-negotiable.

Cost attribution also belongs in your database. There is no tag-aggregated cost reporting API, so record campaign and event cost dimensions at send time and reconcile provider metadata later. For reusable SMS copy, template management exists, but keep an application catalog and mapping because the template list surface is not available for the operational query you need.

A rollout that preserves evidence

Start in shadow mode: resolve preferences and suppression lists, write rows, and compare the projected audience with the current sender. Then enable one channel for a small tenant cohort, poll until terminal states, and inspect the audit export before widening the batch size. Test duplicate jobs, a worker crash after submission, a delayed provider event, a revoked preference, and a full suppression list. Those are mundane tests; they are also the incidents that make a compliance report credible.

Finally, measure freshness separately from delivery success. Track queue age, polling lag, terminal-state coverage, and the count of unknown rows older than your policy threshold. The system is ready when an auditor can follow one event from preference snapshot through batch request to final observed status, with no inference required.

References

Top comments (0)