5.7.6 Per-user notification preferences — `NotificationPreference` model + the settings page (Jira shape); the channel gate BOTH the 5.7.3 in-app job AND the DONE 5.1.6 email job honour
Estimate: 38m · Depends on: 5.7.2, 5.7.1
The per-user notification preferences (the Jira personal-notification-settings shape) + the channel gate that makes them real. Design-gated (5.7.1, the preferences panel) and built on the 5.7.2 foundation. Full 4-layer for the model + settings surface.
NotificationPreference model (prisma/schema.prisma + migration, two-sided FK): { id, userId, eventType, channel, enabled } with @@unique([userId, eventType, channel]) — a per-user × event-type × channel (email | in_app) toggle. Absence = the documented default (direct/mention events default both channels ON; the resolver supplies the default so an unset row is not "off"). userId FK a two-sided @relation (User.notificationPreferences, onDelete Cascade). prisma migrate dev reports no drift.
The resolver — the channel gate (the load-bearing seam). lib/services/notificationPreferencesService.ts exposes isChannelEnabled(userId, eventType, channel) (and a batch variant) resolving a row-or-default. This is the SAME gate consulted by:
• the 5.7.3 in-app job (the call site it stubbed with a permissive default — now backed by the real resolver), AND
• the DONE 5.1.6 email job — wired here to consult isChannelEnabled(recipient, eventType, "email") before sending, so toggling email OFF actually suppresses the mail. This is a gate ADDED at 5.1.6's SEND decision, not a change to any emit site — the event still fires once; the email job just asks the resolver before dispatching (the one-emit-path invariant holds). 5.1.6 already documents the preference toggle as its seam (its description), so this is the planned wiring, not a reach into frozen code.
The settings page (app/(authed)/settings/account/_components/ — the existing personal-settings area, beside LanguageCard): the per-event-type × channel matrix of toggles from the 5.7.1 design (Mentioned / Commented / Assigned / [Watching: transitioned — disabled 5.4 seam] × Email · In-app), with the Watching-event rows drawn disabled until 5.4 lands. Toggling a cell is an inline mutation through a Server Action calling the service — the success response IS the confirmation (no whole-tree refresh; the inline-edit memory), with saving/saved/error inline states. A GET/PUT /api/notification-preferences route pair (HTTP-only) backs it.
Acceptance criteria
NotificationPreference(@@unique([userId, eventType, channel]), two-sided@relation, Cascade) + migration land;prisma migrate devreports no drift; an unset preference resolves to the documented default (direct/mention ON), not "off".notificationPreferencesService.isChannelEnabledis the single resolver; the 5.7.3 in-app job consults it (in_app) AND the DONE 5.1.6 email job is wired to consult it (email) at its SEND decision — toggling email off stops the mail, toggling in-app off stops the bell row, with NO change to any emit site (the event still fires once).- The settings page renders the event × channel matrix on
/settings/accountwith the Watching-event rows disabled (the 5.4 seam); toggling persists through a Server Action whose success response updates the cell (norouter.refreshfan-out); saving/saved/ error states match the 5.7.1 design. - Routes are HTTP-only;
pnpm test:coverageholds the gate on the new model/service/ page code.
Context refs
- 5.7.2 (the
Notificationmodel + thetypeaxis the preference keys on); 5.7.3 (the in-app call site this backs); 5.7.1 (the preferences-panel design) lib/jobs/definitions/mentionNotify.ts(5.1.6, DONE) — the email job whose SEND decision this gates (the documented preference seam in its 5.1.6 card)app/(authed)/settings/account/page.tsx+_components/LanguageCard.tsx— the existing personal-settings area + card grammar to extend;components/ui/Switch+ContentSectionCardmotir-core/CLAUDE.md— 4-layer; the inline-edit-no-whole-tree-refresh memory (the toggle success IS confirmation); the Jira Personal settings → Notification settings mirror