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>
48 lines
2.1 KiB
Markdown
48 lines
2.1 KiB
Markdown
# 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.
|