Comms provider registry seam — channels, fast validation, async send
Comms provider registry seam
One messaging service owns all outbound/inbound comms (email now, SMS/voice adapters present). Every send targets a named channel; a channel binds a provider adapter to a sender policy (allowed From identity/domain). Provider selection is a registry seam in core: Registry.Send(channel, msg) — new providers/channels are added without touching core logic.
Problem it solves
- Products need email/SMS/voice across many providers (user SMTP, workspace SMTP, MS Graph, AWS SES, AU SMTP relay) with per-sender policies; hard-coding providers into business logic makes every new provider a refactor and every send a potential compliance bypass.
Shape
- Channels (proven):
user(SMTP, end user's mailbox),workspace(SMTP, org mailbox),ms(Microsoft Graph sendMail, SMTP fallback),ses(AWS SES v2 SendEmail REST, default bulk),au_smtp(cheapest AU relay, config-driven). - Fast validation then return:
POST /v1/messagesdoes only cheap bounded work — schema/address syntax, channel + sender-policy validation, suppression check, optional MX check (≤500ms, cached) — persistsqueued+ audit, enqueues, returns202. All provider I/O is async in the worker. - Worker path: load message → re-check suppression → resolve channel → provider send →
sent/failedwith retryable backoff → undeliverability webhook on terminal failure. - Runs serverless (Lambda/API GW/SQS/DynamoDB) and standalone (SQLite + in-process queue) — same core, env-selected adapters.
Rules
- Suppression is checked twice: at intake (fast reject) and again in the worker (state may have changed between queue and send).
- Sender policy is per channel: a channel may only send as its allowed From identity/domain — enforced at validation, not convention.
- Provider I/O never blocks the request path — validate, persist, enqueue, return 202.
- New providers are adapters behind the registry, never core changes.
- Every state change writes an audit record.
Anti-patterns
- Provider calls in the request path (latency + provider outage = API outage).
- Single global sender identity — policy belongs to the channel.
- Suppression checked once at intake only (queued-then-unsubscribed sends still go out).
- Provider-specific logic leaking into core (bounce formats, retries).
Sources
- factory/user-channels/REQUIREMENTS.md (§2.1 channels, §2.2 fast validation, §2.3 worker)
- factory/user-channels/internal/sender/{msgraph,sessender,smtpsender}, internal/awsmini (SigV4 without SDK)
- factory/user-channels/sms/, voice/ (SMS DLR + voice adapters)