Live demo — shared accounts, sample data, reset regularly. Don't enter anything real.Get the source

Notifications

Notifications

What it does

A generalized dispatcher for "tell a user something happened" — always writes an in-app notification (the bell in the app header), and optionally also sends an email, gated by a per-user preference the user controls themselves in /settings.

How to configure it

No new env vars — this reuses RESEND_API_KEY/EMAIL_FROM from email.md for its email half (a notify() call with no email option skips that entirely, no config needed at all for in-app-only events).

How it works

  • lib/notify.ts's notify({ userId, type, title, body?, link?, email? }) is the one call site every "tell a user something" flow goes through:
    1. Always inserts a row into the notifications table (lib/db/schema/app.ts — type is free-form dot-namespaced text, same convention as lib/audit.ts's action, since the two overlap heavily: a role change or a new subscription is both audit-worthy and notification-worthy).
    2. If email is present, looks up notification_preferences.emailEnabled for that user (defaults to true — the table only gets a row once a user actually changes the setting, see getEmailPreference/ setEmailPreference) and sends it via lib/email.ts's sendEmail() if still enabled. Best-effort throughout — same non-critical error handling as lib/audit.ts's logAudit, a notification failing to send must never break the action that triggered it.
    • Channels are a closed set: NotifyChannel is "db" | "email". Widen the type and the dispatch logic in notify() to add another.
  • Call sites (lib/auth.ts) — the events that currently notify:
    • organizationHooks.afterUpdateMemberRole — in-app only (there's no email template for this event yet).
    • organizationHooks.afterCreateInvitation — in-app only, and only when the invited email already belongs to a user row (an invitee with no account has nowhere to put a bell notification; they still get the invitation email regardless, via sendInvitationEmail, unconditionally).
    • Stripe onSubscriptionComplete/onSubscriptionCancel — in-app + email, reusing the SubscriptionStartedEmail/SubscriptionCanceledEmail templates from email.md. Routing them through notify() rather than calling sendEmail() directly is what makes the email half respect the user's preference. Each also fans out an in-app-only notification to every site admin via notifyAdmins() (lib/auth.ts).
  • components/notifications/notification-bell.tsx — the bell + dropdown in app/(app)/layout.tsx's header. The layout fetches the 10 most recent notifications + an unread count per request and passes them down; clicking an item calls markNotificationRead() (lib/actions/notifications.ts) and navigates to its link if it has one. Polling, not websockets: a 20s setInterval(() => router.refresh()) re-runs the layout's server-side fetch so a notification created by another request (a webhook, an admin action) shows up without a manual reload.
  • app/(app)/settings/page.tsx's NotificationPreferencesForm — the one user-facing toggle, emailEnabled. In-app notifications are never optional (that's the whole point of the bell); this only controls whether matching events also email.

How to extend it

  • A new notification-worthy event — call notify() from wherever it happens (a Server Action, a better-auth hook), same as the call sites above. Pass email: { subject, react } only if you also want it emailed — build the React Email template the same way as emails/*.tsx (see email.md).
  • Per-notification-type email preferences (instead of one global toggle) — widen notification_preferences past its single emailEnabled column; getEmailPreference/setEmailPreference in lib/notify.ts are the only two places that would need to change shape.
  • Push-based delivery (SSE/WebSocket instead of polling) — replace the setInterval in notification-bell.tsx; the server-side data shape (notifications table, the layout's fetch) doesn't need to change.

How to remove it

Delete lib/notify.ts + its test, lib/actions/notifications.ts, components/notifications/, components/settings/notification-preferences-form.tsx, the notify()/ notifyAdmins() calls and notifyAdmins helper from lib/auth.ts (the logAudit() calls next to them are independent — see security.md — leave those), the notifications/notification_preferences tables from lib/db/schema/app.ts (regenerate a migration), and the bell + its data fetch from app/(app)/layout.tsx.