Files
clean-architecture-backend-…/docs/notification/adr/NOTIF-ADR-004-fcm-fid-primary.md
T
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

1.2 KiB

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.