learning
One event, three delivery mechanisms
One event, three delivery mechanisms
One logical event — "your request status changed" — needs to reach the user through genuinely different mechanisms: an in-app notification (a row shown next time they open the app), a web push (through the browser's Push API and a service worker), a mobile push (routed through Expo's push service). Each has different failure modes. From the user's side it's one thing: "notify me."
Separate the event from the delivery
notifications — the logical event, what happened and for whom — is kept separate from notification_deliveries — each delivery attempt on a specific channel. One event fans out into several delivery rows, each tracked independently with its own status and channel. A failure on one channel — a stale web push subscription — doesn't affect or get conflated with the others for the same event.
notification_preferences gives per-event, per-channel control rather than one global toggle. A user can want in-app plus email for status changes and explicitly not want a mobile push interrupting them for every chat message. That granularity is what respects real behaviour.
Delivery goes through the same transactional outbox as email — the event is written durably first, then the processor fans it out across whichever channels preferences say should receive it, retrying failed channel-specific deliveries independently rather than the whole notification succeeding or failing as one unit.
Above individual preferences sits an operator-level toggle — an organisation could disable web push for their whole community, which constrains what individual users can even opt into. Two layers: "is this channel available here at all" and "do I want this on this channel," both checked before attempting delivery.
Notifications are easy to bolt on late as a shallow sendEmail() call, but they touch identity, preference, channel technicalities and reliability all at once. Treating it as its own domain — its own tables, preference model, delivery tracking — pays off once there's more than one channel and more than one notification type, which happens faster in a real product than it looks at the start.