Separate the business event, notification, and delivery attempt
A comment, invoice, or approval request can create several messages for several recipients. Keeping all of that state in one email job makes preferences, unread counts, retries, and support investigations difficult. Model a business event, a recipient-specific notification, and each channel attempt separately.
Event
What happened, which tenant owns it, and the stable event ID.
Notification
Who should know, why, which resource it concerns, and when it becomes irrelevant.
Delivery
Which channel was selected, its attempt state, provider ID, and safe outcome.
For example, an approval request can remain unread in the application inbox even after its email is delivered. Acknowledging the business task is another state entirely. Keep those meanings distinct in the schema and user interface.
Create notification intent beside the committed change
Record the source event in an outbox in the same transaction as the business mutation. A worker can then resolve eligible recipients and create notification rows with a unique key such as event ID, recipient ID, and notification type. Retries should converge on the same intent.
Resolve membership and permissions on the server, filter out the actor when appropriate, and bound fan-out for large organizations. Process recipients in resumable batches so one popular event cannot occupy the entire worker pool. The transactional outbox guide explains the commit boundary.
Make preferences understandable and enforce them at send time
Offer settings by understandable category and channel: mentions, approvals, project activity, or account alerts. Store defaults and explicit overrides with a policy version. Explain any mandatory operational categories clearly rather than hiding exceptions behind a general switch.
Apply timezone-aware quiet hours and frequency choices where useful. Recheck current preferences and recipient eligibility before dispatching delayed work, because a user may opt out, leave the tenant, or lose access after the event was created. Persist a safe suppression reason so support can distinguish an intentional skip from a failure.
Build the inbox around recipient-scoped queries
Query notifications by trusted user and tenant context, then paginate with a stable ordering such as creation time plus ID. Never let a submitted recipient ID choose another inbox. Recheck access when rendering sensitive content and when the destination link opens.
Use idempotent read and unread mutations. Define whether read state is shared across devices and keep it independent from delivery status. For mark-all-read, capture a server-side cutoff so notifications arriving during the request remain unread. Derive or reconcile unread counters instead of assuming every optimistic client increment is authoritative.
Use concise titles, meaningful action labels, timestamps, and a visible unread indicator that does not rely on color alone. Preserve keyboard focus when the list changes and announce count changes sparingly.
Give each channel an independent delivery lifecycle
Email, mobile push, and the application inbox have different guarantees. Record queued, attempting, accepted, failed, suppressed, and expired states as appropriate for each channel. Provider acceptance is not proof that a person saw or read a message.
Retry transient failures with backoff, jitter, and an attempt limit. Deduplicate by notification and channel, retain provider IDs, and treat uncertain timeouts carefully. Invalid destinations should be suppressed or retired according to channel policy. Our transactional email guide covers email-specific delivery evidence and webhooks.
Keep sensitive data out of lock-screen previews and message subjects. Prefer a short summary and an authenticated deep link when the underlying record contains private information.
Use real-time events to refresh durable state
A WebSocket or event stream can tell the browser that the inbox changed, but it should not be the only copy of a notification. On initial load and reconnect, fetch the durable inbox and authoritative count. Deduplicate live hints and recover from dropped, duplicated, or reordered events.
Authorize subscriptions by user and tenant, clean them up when sessions or memberships change, and avoid broadcasting private payloads to a tenant-wide room. If polling is simpler for the product, use bounded intervals, visibility-aware refresh, and a last-seen cursor to reduce unnecessary work.
Group low-priority activity without changing its meaning
Digest jobs can group repeated activity by recipient, category, and time window. Keep the original notification IDs so each item remains traceable, and deduplicate the digest send separately. Re-evaluate permissions before rendering the digest rather than exporting a stale snapshot of private content.
Set expiry for time-sensitive prompts. A resolved approval request or canceled meeting should not create a fresh alert hours later merely because a queue recovered. Decide whether to suppress, replace, or summarize superseded activity and record the outcome.
Measure useful outcomes and test lifecycle changes
Track generation lag, fan-out size, queue age, channel acceptance, retries, suppression, expiry, unread reconciliation, and preference changes. Use notification IDs and safe reason codes in logs, with retention that matches the product policy.
Test duplicate events, delayed workers, quiet-hour boundaries, membership revocation, expired sessions, provider outages, concurrent mark-all-read requests, and reconnecting clients. Verify that one domain event produces the intended notification set and that a preference change takes effect before delayed delivery.
Use the ownership and retry controls in our background-work guide for digests and channel dispatch.
Next.js notification system checklist
✓ Business events, recipient notifications, and attempts are separate
✓ Creation and fan-out are durable and idempotent
✓ Current preferences and permissions are checked before delivery
✓ Read state and delivery evidence have distinct meanings
✓ Inbox queries and live subscriptions enforce recipient scope
✓ Reconnects recover from the durable inbox
✓ Digests and stale prompts have explicit expiry rules
✓ Operational metrics expose lag, suppression, and failures
Build a more dependable Next.js application
Endurance Softwares helps teams design, build, test, and operate production-ready Next.js platforms.
