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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b5aee50e3
commit
701ba67456
@@ -0,0 +1,47 @@
|
||||
# Operations
|
||||
|
||||
## Runtime shape
|
||||
|
||||
```text
|
||||
durable queue (PostgreSQL, FOR UPDATE SKIP LOCKED)
|
||||
→ expiry check
|
||||
→ suppression and eligibility re-check
|
||||
→ provider health gate
|
||||
→ rate limiter
|
||||
→ concurrency limiter
|
||||
→ provider adapter
|
||||
```
|
||||
|
||||
Provider calls run outside every database transaction. The attempt row is committed first, so after a
|
||||
crash the row is either absent (nothing was sent) or present in `DISPATCHING` (reconciliation has
|
||||
something to ask about).
|
||||
|
||||
## Guards that exist for specific incidents
|
||||
|
||||
| Guard | The incident it prevents |
|
||||
|---|---|
|
||||
| Credential failure opens the provider route | One expired key multiplied by a queue becomes a self-inflicted outage |
|
||||
| Retry budget per provider profile | A provider outage turning every queued notification into its own retry loop |
|
||||
| Ambiguous attempts block automatic fallback | A push whose response was lost arriving alongside the "just in case" SMS |
|
||||
| Permits released during backoff | A slow provider pinning the whole concurrency budget on work that is only waiting |
|
||||
| Bounded drain on rotation | A provider that never answers holding a credential rotation open forever |
|
||||
| Fail-fast intake on capacity | An unbounded in-memory queue absorbing a burst it cannot survive |
|
||||
|
||||
## Scheduling
|
||||
|
||||
`scheduleAt` activates the job, `notBefore` is the earliest permitted provider submission, and
|
||||
`expiresAt` blocks new attempts, retries and fallbacks. Suppression and expiry are re-checked
|
||||
immediately before dispatch, because a scheduled notification can sit in the queue for hours and the
|
||||
user may have opted out in the meantime.
|
||||
|
||||
## Redrive
|
||||
|
||||
A redrive preserves `NotificationId` and `RecipientDeliveryId`, creates a new `DeliveryAttemptId`, and
|
||||
reuses the pinned template version and rendered digest. Sending different content is a new
|
||||
notification, not a redrive. Redriving an ambiguous attempt requires explicit duplicate-risk approval,
|
||||
because the platform genuinely cannot tell whether the first submission reached the user.
|
||||
|
||||
## Actuator surface
|
||||
|
||||
Provider runtime states and generations, queue depth and age, callback and reconciliation health.
|
||||
Never addresses, never credentials.
|
||||
Reference in New Issue
Block a user