feat: broadcast
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
-- 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)
|
||||
);
|
||||
Reference in New Issue
Block a user