-- 015: WP15 Telegram live channel archive. All identities are tenant pinned. CREATE TABLE feed_subscriptions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id uuid NOT NULL, type text NOT NULL CHECK (type = 'telegram_channel'), connection_id uuid NOT NULL, channel_id text NOT NULL CHECK (btrim(channel_id) <> ''), health text NOT NULL DEFAULT 'active' CHECK (health IN ('active', 'bot_removed', 'admin_required')), health_checked_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (workspace_id, connection_id, channel_id), UNIQUE (id, workspace_id), FOREIGN KEY (connection_id, workspace_id) REFERENCES connections (id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT ); CREATE INDEX feed_subscriptions_channel ON feed_subscriptions (workspace_id, channel_id); CREATE TABLE archived_feed_items ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, remote_ref text NOT NULL CHECK (remote_ref ~ '^(-?[0-9]+|@[A-Za-z0-9_]+):[0-9]+$'), channel_id text NOT NULL, message_id bigint NOT NULL CHECK (message_id > 0), media_group_id text, text_content text NOT NULL DEFAULT '', content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'), revision integer NOT NULL DEFAULT 1 CHECK (revision > 0), media_refs jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(media_refs) = 'array'), attribution jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(attribution) = 'object'), protected_content boolean NOT NULL DEFAULT false, posted_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (workspace_id, remote_ref), UNIQUE (id, workspace_id), CHECK (remote_ref = channel_id || ':' || message_id::text) ); CREATE INDEX archived_feed_items_cursor ON archived_feed_items (workspace_id, posted_at DESC, id DESC); CREATE TABLE archived_feed_revisions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id uuid NOT NULL, item_id uuid NOT NULL, revision integer NOT NULL CHECK (revision > 0), content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'), text_content text NOT NULL, media_refs jsonb NOT NULL CHECK (jsonb_typeof(media_refs) = 'array'), attribution jsonb NOT NULL CHECK (jsonb_typeof(attribution) = 'object'), created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (workspace_id, item_id, revision), FOREIGN KEY (item_id, workspace_id) REFERENCES archived_feed_items (id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT ); -- drafts predates tenant-pinned references. ALTER TABLE drafts ADD CONSTRAINT drafts_id_workspace_key UNIQUE (id, workspace_id); CREATE TABLE archived_feed_drafts ( workspace_id uuid NOT NULL, item_id uuid NOT NULL, draft_id uuid NOT NULL UNIQUE, created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (workspace_id, item_id, draft_id), FOREIGN KEY (item_id, workspace_id) REFERENCES archived_feed_items (id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT, FOREIGN KEY (draft_id, workspace_id) REFERENCES drafts (id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT ); CREATE TRIGGER trg_feed_subscriptions_ws_immutable BEFORE UPDATE ON feed_subscriptions FOR EACH ROW EXECUTE FUNCTION reject_workspace_change(); CREATE TRIGGER trg_archived_feed_items_ws_immutable BEFORE UPDATE ON archived_feed_items FOR EACH ROW EXECUTE FUNCTION reject_workspace_change(); CREATE TRIGGER trg_archived_feed_revisions_ws_immutable BEFORE UPDATE ON archived_feed_revisions FOR EACH ROW EXECUTE FUNCTION reject_workspace_change(); CREATE TRIGGER trg_archived_feed_drafts_ws_immutable BEFORE UPDATE ON archived_feed_drafts FOR EACH ROW EXECUTE FUNCTION reject_workspace_change(); ALTER TABLE feed_subscriptions ENABLE ROW LEVEL SECURITY; ALTER TABLE archived_feed_items ENABLE ROW LEVEL SECURITY; ALTER TABLE archived_feed_revisions ENABLE ROW LEVEL SECURITY; ALTER TABLE archived_feed_drafts ENABLE ROW LEVEL SECURITY; ALTER TABLE feed_subscriptions FORCE ROW LEVEL SECURITY; ALTER TABLE archived_feed_items FORCE ROW LEVEL SECURITY; ALTER TABLE archived_feed_revisions FORCE ROW LEVEL SECURITY; ALTER TABLE archived_feed_drafts FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON feed_subscriptions USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid); CREATE POLICY tenant_isolation ON archived_feed_items USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid); CREATE POLICY tenant_isolation ON archived_feed_revisions USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid); CREATE POLICY tenant_isolation ON archived_feed_drafts USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid); GRANT SELECT, INSERT, UPDATE, DELETE ON feed_subscriptions, archived_feed_items, archived_feed_revisions, archived_feed_drafts TO sp_app;