Files
DongHyeonkaandClaude Opus 5 701ba67456 feat(notification): implement the notification delivery platform
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>
2026-08-14 13:57:27 +09:00

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.