62 lines
2.7 KiB
SQL
62 lines
2.7 KiB
SQL
-- notif.notifications — durable per-user notification records.
|
|
--
|
|
-- Backs the bell UI and the retention job. The message bus is best-effort
|
|
-- (a subscriber offline at publish time misses the push); this table is
|
|
-- the truth. Every `NotificationService::create` writes a row AND
|
|
-- publishes a `NotificationReceived` event on `user:{user_id}:notifications`.
|
|
-- A missed bus event recovers on the next `GET /api/notifications`.
|
|
--
|
|
-- See `docs/plan/message-bus.md § Slice E` for the wire contract and
|
|
-- retention policy.
|
|
|
|
CREATE SCHEMA IF NOT EXISTS notif;
|
|
|
|
CREATE TABLE IF NOT EXISTS notif.notifications (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Recipient. Every row is scoped to exactly one user; a share fanned
|
|
-- to N members is N rows. Fanout truncation for very-large groups
|
|
-- happens in the ingester (see plan § Notification fanout truncated),
|
|
-- not here.
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
|
|
-- Notification kind — a stable slug the FE routes on for icon/label/
|
|
-- action-button choice. New kinds are additive; never repurpose an
|
|
-- existing one. Initial kinds:
|
|
-- share_granted, new_login_from_new_device,
|
|
-- job_completed_for_you, storage_quota_threshold
|
|
kind TEXT NOT NULL,
|
|
|
|
-- Per-kind opaque JSON with the fields the FE needs to render the
|
|
-- row without a follow-up API call (subject name, resource id,
|
|
-- action link…). Shape is a per-kind contract owned by the ingester;
|
|
-- the DB stays schema-free here so a new field doesn't require a
|
|
-- migration.
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
|
|
-- Wall-clock creation stamp. Sort key for the bell. Server-clock,
|
|
-- not caller-clock — this is a DB-generated fact.
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
-- NULL = unread; non-NULL = when the user explicitly marked it
|
|
-- read. Retention job deletes rows where read_at IS NOT NULL AND
|
|
-- read_at < now() - retention_days.
|
|
read_at TIMESTAMPTZ
|
|
);
|
|
|
|
-- Bell fetch — GET /api/notifications lists a user's rows newest-first,
|
|
-- typically capped at ~50, sometimes filtered on unread. This one index
|
|
-- covers both the list query and the mark-all-read filter, and the
|
|
-- INCLUDE clause keeps common bell renders (id, kind, created_at,
|
|
-- read_at) index-only.
|
|
CREATE INDEX IF NOT EXISTS notifications_user_created_read
|
|
ON notif.notifications (user_id, created_at DESC)
|
|
INCLUDE (read_at, kind);
|
|
|
|
-- Retention job DELETE — scans read-and-old rows only. Partial keeps
|
|
-- the index tiny in the typical steady state where most rows are
|
|
-- unread.
|
|
CREATE INDEX IF NOT EXISTS notifications_read_at
|
|
ON notif.notifications (read_at)
|
|
WHERE read_at IS NOT NULL;
|