Notifications
In-app alerts, idempotency, collapsing, and system broadcasts.
The notifications slice dictates what to tell a user and when. It owns the in-app notification centre, system-wide broadcasts, and user notification preferences.
The Delivery Invariant
notify() service function is designed to never throw for a delivery reason. A notification failing to send (e.g. database timeout or email enqueue failure) must not roll back the upstream action that triggered it. Notifications depend on the mail slice one-way to enqueue digests, but the mail slice never depends on notifications.UserNotification Invariants
The UserNotification model is heavily engineered to protect users from alert spam and to protect the database from race conditions.
Collapsing (groupKey)
Instead of creating 40 separate rows if a user experiences 40 failed sends, the system uses a groupKey (e.g. reply:<applicationId>). If an unread notification with the same groupKey already exists within a specific time window, the system simply increments its count field.
Structural Idempotency (dedupeKey)
To prevent duplicate notifications from race conditions, producers can supply a strict dedupeKey (e.g. reply:<gmailMessageId>).
This uses a clever database trick: the schema applies a unique constraint on @@unique([userId, dedupeKey]). Because Postgres treats NULL values as distinct in a unique index, producers that don't need idempotency (and pass null) pay no penalty. But for strict events, this constraint structurally fixes live bugs—for example, preventing the Gmail cron job and the real-time Pub/Sub webhook from both firing a notification for the exact same incoming reply.
Rich Metadata
Every notification stores a JSON metadata blob containing the originating payload. This ensures the UI has all the necessary context to render rich, interactive alerts long after the event occurred.
Broadcasts
System-wide announcements are handled by the Broadcast model (categorized as info, warning, or maintenance).
Because creating a notification row for every single user on the platform would be devastating to the database, Broadcasts are a single row. The system only writes to the BroadcastDismissal join table when a user actively dismisses the banner, maintaining high performance at scale.