Maps the 31-module plan onto the registry's 19 leaves as packages; the two edges the registry forbids (provider->httpclient, inbox->messaging) are replaced by application-owned ports. See docs/notification/module-mapping.md. Acceptance is not delivery: ProviderSubmissionResult refuses to carry a delivery outcome, and AMBIGUOUS is a first-class terminal state that blocks automatic retry and fallback until reconciliation resolves it. Providers: SES (SigV4 + SNS callback), Twilio (X-Twilio-Signature + reconciliation), FCM (FID-primary batch), APNs, Web Push (RFC 8030/8291/8292), SMTP and webhook. Contact points are AES-256-GCM encrypted with a separate HMAC lookup fingerprint; nothing raw reaches a log, metric tag or exception. Dispatch commits the attempt row, calls the provider with no transaction open, then records the outcome; the durable queue uses FOR UPDATE SKIP LOCKED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2.7 KiB
Provider runbooks
SMTP
| Symptom | Classification | Action |
|---|---|---|
Final 2xx after DATA |
CONFIRMED_ACCEPTED / PROVIDER_ACCEPTED |
None; this is acceptance, not inbox delivery |
4yz |
TRANSIENT_PROVIDER |
Retry under budget and deadline |
5yz |
PERMANENT_PROVIDER or INVALID_RECIPIENT |
Stop, or invalidate the contact point |
Connection lost after DATA |
AMBIGUOUS_SUBMISSION |
Reconcile or escalate; do not resend automatically |
Connection, read, write and pool-acquire timeouts are all finite. There is no unbounded timeout.
Amazon SES
MessageId is acceptance evidence. SES itself documents that it can accept a request and then not
send, so MessageId is never mapped to DELIVERED.
| Event | Normalized |
|---|---|
Send |
reinforces PROVIDER_ACCEPTED |
Delivery |
DELIVERY_CONFIRMED / NETWORK_OR_CARRIER_ACCEPTED |
DeliveryDelay |
delay fact |
Bounce (permanent) |
BOUNCED_HARD plus hard-bounce suppression |
Bounce (transient) |
BOUNCED_SOFT; retry policy input, not a suppression reason |
Complaint |
complaint fact plus suppression |
Reject |
PROVIDER_REJECTED |
RenderingFailure |
TEMPLATE_FAILURE |
Twilio
accepted/queued is acceptance only. sent is carrier acceptance. delivered is device delivery.
Callbacks are not ordered. A sent arriving after delivered is stored and ignored by the
projection. Missing callbacks are corrected by status polling under the provider rate limit.
Signature verification uses the canonical external URL from the profile, not the URL the servlet container reconstructed behind a proxy.
FCM
| Error | Classification |
|---|---|
UNREGISTERED |
INVALID_RECIPIENT; invalidate the contact point, never retry |
INVALID_ARGUMENT |
INVALID_PAYLOAD |
QUOTA_EXCEEDED |
THROTTLED, exponential backoff |
UNAVAILABLE |
TRANSIENT_PROVIDER, honour Retry-After, add jitter |
| Credential failure | AUTHENTICATION; opens the provider route |
A batch is one transport call and many attempts. Partial results map back by input index; one transport failure does not become one shared outcome unless the adapter can prove it.
APNs
2xx is acceptance. Environment and topic mismatches are configuration failures, not delivery failures. Sandbox and production tokens are separate namespaces.
Web Push
TTL is mandatory by protocol. 201 is acceptance. 404 is an expired subscription per RFC 8030;
provider-documented 410 maps the same way. Payloads use aes128gcm per RFC 8291 and VAPID JWTs are
signed per RFC 8292 with the audience taken from the endpoint origin.
VAPID key rotation is not ordinary credential rotation: a restricted subscription may need to be re-created, so it is a migration operation.