# Notification foundation

Juris-PMS notifications are durable, serious attention signals. They are not a second activity stream.

## Persistence model

`notification_events` stores one channel-agnostic occurrence: its stable event key, taxonomy, tenant scope, actor, stable target/context references, minimal safe payload, deduplication key, and occurrence time. `user_notifications` projects that occurrence into one row per recipient and stores the recipient reason plus nullable seen/read timestamps. The database is the source of truth for all future delivery channels.

## Taxonomy

Stage 1 implements the categories `assignment`, `blocking`, `membership`, and `access_permissions`; priorities `critical`, `action_required`, `important`, and `informational`; and preference classifications `mandatory`, `default`, `optional`, and `activity_only`. Definitions live in `NotificationCatalogue`; unknown event keys are rejected.

Implemented event keys are `work_item.assigned`, `work_item.reassigned`, `work_item.unassigned`, `work_item.blocked`, `work_item.blocker_resolved`, `project.membership_added`, `project.membership_removed`, `organization.access_changed`, and `project.access_changed`.

## Recipient and security rules

Candidates are supplied by the domain action that owns the business change. Resolution requires current organization membership and, normally, current project membership. Work-item recipients must also pass the authoritative `WorkItemPolicy::view` check. Actors are suppressed by default, and candidates are deduplicated by user. Project-membership removal is the narrow exception to current project access because the affected user must be told that access was removed.

Notification records never grant access. Targets must be reauthorized when a future UI resolves them. Payloads remain minimal and do not contain descriptions, note bodies, attachments, or other protected content.

## Atomicity and deduplication

Notification recording runs inside the successful business transaction. A unique event dedupe key makes logical replay idempotent; a unique event/recipient constraint prevents duplicate inbox rows. Stage 1 dispatches no external work, which avoids the queue connection's non-after-commit default.

## Stage 1 integrations

Assignments are recorded by work-item create/update actions. Blocking is recorded by the link create/delete actions; `relates_to` stays activity-only. Project membership add/remove and effective project-role permission changes notify only the affected user. Routine edits, attachments, sprint ordering, ordinary workflow changes, notes, and general activity do not notify.

## Deferred

The bell, dropdown, history/settings UI, notification endpoints, polling, Web Push, service workers, realtime/WebSockets, email, digest, preferences, deadlines, mentions, review/QA, sprint notifications, AI alerts, exact deep links, archiving, and external delivery attempts are explicitly deferred.

## Browser Web Push

Web Push is an optional delivery channel for existing `user_notifications`; the database inbox remains authoritative. Eligibility, per-category Web Push preferences, and per-browser subscriptions are separate. One inbox record may therefore produce deliveries to multiple active devices. Demo notifications bypass the recorder and never dispatch delivery jobs.

The implementation uses `minishlink/web-push` v11.0.0 with VAPID. Generate a key pair once with `php artisan notifications:vapid-generate`, then configure `VAPID_SUBJECT`, `VAPID_PUBLIC_KEY`, and `VAPID_PRIVATE_KEY`. Never commit the private key or rotate keys casually because existing subscriptions will stop working.

Users enable a browser only through an explicit Profile action. Denied and unsupported states are reported without repeated permission prompts. The small `/notification-sw.js` worker receives privacy-limited title/body/open-route payloads, displays them, and focuses or opens Juris-PMS on click. It does not cache pages or provide offline/PWA behavior.

Delivery jobs are explicitly dispatched after commit and use the configured queue. `composer run dev` already starts `php artisan queue:listen --tries=1`; alternatively run `php artisan queue:work --tries=3`. Production requires HTTPS, valid VAPID configuration, a persistent supervised queue worker, and a supported browser. `localhost` may use the browser secure-context exception.

Expired (404/410) subscriptions are deactivated independently. Temporary failures use bounded queue retries. Payloads contain no descriptions, comments, attachments, permission lists, raw target IDs, or subscription secrets; clicks go through an authenticated owned-notification route and the existing destination resolver.

Manual QA: migrate, configure VAPID, start the queue, open Profile → Notifications, explicitly enable Browser notifications and accept permission, then use a second account to assign or block a Work Item. Confirm one inbox record, a processed queue delivery, an OS notification, and a click to the authorized Project Worklog that marks the notification read. Disable the current-device switch, trigger another event, and confirm that device receives no push. Repeat in light and dark themes and verify category switches independently.

WebSocket realtime, email/digest, and native desktop applications remain deferred. Existing 45-second polling remains enabled for active-page synchronization.
