-- M489: a persistent, per-user notifications inbox (#726). -- -- The event bus is fire-and-forget: a client that is not connected when a -- request completes, or when the scanner marks tracks missing, never hears of -- it. A row here is the durable record; the bus only nudges open clients to -- come and read it. -- -- `kind` is CHECK-gated (rule 36). A new kind adds its value here, in the -- same migration as the code that produces it. The list is mirrored in -- internal/notifications/kinds.go, and both CHECKs below must agree with it. CREATE TABLE user_notifications ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, kind text NOT NULL CHECK (kind IN ( 'request_approved', 'request_rejected', 'request_completed', 'request_pending', 'quarantine_flagged', 'scan_failed', 'tracks_missing', 'duplicates_found', 'playback_errors' )), payload jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now(), read_at timestamptz, -- Burst-prone kinds share one key per user. While a row with that key is -- unread, a new event updates it in place rather than adding another, so -- fourteen missing tracks are one notification with a count. coalesce_key text, -- Stamped once the row has gone out in an email (or been judged not to -- need one), so the digest never sends the same item twice. emailed_at timestamptz ); -- The inbox list, newest first. CREATE INDEX user_notifications_user_created_idx ON user_notifications (user_id, created_at DESC); -- The unread badge, and the digest's selection of unread rows. CREATE INDEX user_notifications_unread_idx ON user_notifications (user_id, created_at DESC) WHERE read_at IS NULL; -- At most one unread row per coalesce key per user. The upsert in -- notifications.sql targets this index. CREATE UNIQUE INDEX user_notifications_coalesce_idx ON user_notifications (user_id, coalesce_key) WHERE read_at IS NULL AND coalesce_key IS NOT NULL; -- Per user, per kind: which channels a kind reaches. A missing row means the -- kind's defaults (internal/notifications/kinds.go), so nothing is seeded and -- a new kind needs no backfill. CREATE TABLE user_notification_prefs ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, kind text NOT NULL CHECK (kind IN ( 'request_approved', 'request_rejected', 'request_completed', 'request_pending', 'quarantine_flagged', 'scan_failed', 'tracks_missing', 'duplicates_found', 'playback_errors' )), inbox boolean NOT NULL, phone boolean NOT NULL, email boolean NOT NULL, updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (user_id, kind) );