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>
28 lines
1.2 KiB
Markdown
28 lines
1.2 KiB
Markdown
# NOTIF-ADR-004 — FCM installation id is the primary target
|
|
|
|
## Status
|
|
|
|
Accepted.
|
|
|
|
## Context
|
|
|
|
Firebase now recommends the installation id (FID) and treats registration-token multicast paths as
|
|
legacy. A contact point model built on a single `token` string would encode the older model as the
|
|
only one, and a later migration would be a runtime interpretation problem: the same string field
|
|
would mean different things for different rows.
|
|
|
|
## Decision
|
|
|
|
`MobilePushTarget` is a sealed hierarchy of `FcmInstallationId`, `LegacyFcmRegistrationToken` and
|
|
`ApnsDeviceToken`. The kinds are separate types, never a discriminator on one string field, and each
|
|
carries its own `ContactPointType` so the uniqueness scope and the encryption associated data differ.
|
|
|
|
APNs tokens additionally carry their environment, because sandbox and production are separate
|
|
namespaces rather than a flag.
|
|
|
|
## Consequences
|
|
|
|
Migrating a target kind is a compile-time change with an exhaustive `switch`, not a runtime guess.
|
|
The adapter maps each kind to its own wire representation, so a provider changing one path cannot
|
|
silently change the other. The cost is one more type than a string field would need.
|