Files
spsays-bff/migrations/0052_broadcasts.up.sql
2026-08-29 18:34:07 +05:30

45 lines
2.3 KiB
SQL

-- Admin broadcasts — product updates and announcements, shown once per user.
--
-- On "no history": these deliberately create no notification rows and never
-- appear in the tray. But "exactly once per user" cannot be done without
-- storing something — the app has to know whether YOU have seen it. So two
-- tables: the announcement, and who has dismissed it. Nothing per-user is
-- written until that person actually sees it.
CREATE TABLE IF NOT EXISTS broadcasts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(200) NOT NULL,
body TEXT NOT NULL DEFAULT '',
-- Formatting as offset/length spans, exactly like chat messages: the same
-- parser produces them and the same component renders them, so no HTML is
-- stored and nothing needs sanitising at render time.
entities JSONB NOT NULL DEFAULT '[]'::jsonb,
-- R2 key for the banner. Public bucket: an announcement is not private,
-- and a signed URL would expire while the modal sat open.
banner_key TEXT,
cta_label VARCHAR(60),
-- App-relative path or absolute http(s) URL; validated in the service.
cta_url TEXT,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-- NULL = draft. Nothing is delivered until this is set.
published_at TIMESTAMPTZ,
-- After this, the announcement stops being offered. This is what keeps a
-- months-old update from greeting someone who signs up tomorrow.
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- "What is live right now?" — the app's pending lookup and the admin list.
CREATE INDEX IF NOT EXISTS broadcasts_live_idx
ON broadcasts (published_at DESC) WHERE published_at IS NOT NULL;
-- One row per person who has seen an announcement. Written on dismissal, which
-- is what makes "at most once" true across devices — localStorage would show
-- it again on every new browser.
CREATE TABLE IF NOT EXISTS broadcast_views (
broadcast_id UUID NOT NULL REFERENCES broadcasts(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (broadcast_id, user_id)
);