T-HUB/apps/api/migrations/006_publishing.sql
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

89 lines
4.1 KiB
PL/PgSQL

-- 006: publishing pipeline (drafts -> approval -> scheduling -> publication)
CREATE TABLE drafts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
author_user_id uuid NOT NULL REFERENCES users(id),
post jsonb NOT NULL,
state text NOT NULL DEFAULT 'editing'
CHECK (state IN ('editing', 'in_review', 'approved', 'rejected', 'scheduled', 'published')),
reviewer_user_id uuid REFERENCES users(id),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE scheduled_publications (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
draft_id uuid NOT NULL REFERENCES drafts(id) ON DELETE CASCADE,
connection_id uuid NOT NULL REFERENCES connections(id),
publish_at timestamptz NOT NULL,
idempotency_key text NOT NULL UNIQUE,
state text NOT NULL DEFAULT 'pending'
CHECK (state IN ('pending', 'in_flight', 'done', 'failed')),
error text,
UNIQUE (draft_id)
);
CREATE TABLE publications (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
draft_id uuid NOT NULL REFERENCES drafts(id) ON DELETE CASCADE,
connection_id uuid NOT NULL REFERENCES connections(id),
idempotency_key text NOT NULL UNIQUE,
remote_id text NOT NULL,
remote_url text,
published_at timestamptz NOT NULL,
provider_metadata jsonb NOT NULL DEFAULT '{}'
);
CREATE INDEX drafts_workspace_state ON drafts (workspace_id, state);
CREATE INDEX scheduled_publications_due ON scheduled_publications (publish_at)
WHERE state = 'pending';
CREATE INDEX publications_draft ON publications (draft_id);
CREATE TRIGGER trg_draft_ws_immutable BEFORE UPDATE ON drafts
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
CREATE TRIGGER trg_scheduled_pub_ws_immutable BEFORE UPDATE ON scheduled_publications
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
CREATE TRIGGER trg_publication_ws_immutable BEFORE UPDATE ON publications
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
ALTER TABLE drafts ENABLE ROW LEVEL SECURITY;
ALTER TABLE scheduled_publications ENABLE ROW LEVEL SECURITY;
ALTER TABLE publications ENABLE ROW LEVEL SECURITY;
ALTER TABLE drafts FORCE ROW LEVEL SECURITY;
ALTER TABLE scheduled_publications FORCE ROW LEVEL SECURITY;
ALTER TABLE publications FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON drafts
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 scheduled_publications
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 publications
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 drafts, scheduled_publications, publications TO sp_app;
-- Narrow SECURITY DEFINER lookups let the scheduler discover tenant context
-- without weakening FORCE RLS for ordinary table access.
CREATE FUNCTION publishing_schedule_workspace(p_idempotency_key text) RETURNS uuid
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
SELECT workspace_id
FROM scheduled_publications
WHERE idempotency_key = p_idempotency_key
$$;
CREATE FUNCTION publishing_due_schedules() RETURNS TABLE(idempotency_key text, publish_at timestamptz)
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
SELECT sp.idempotency_key, sp.publish_at
FROM scheduled_publications sp
WHERE sp.state = 'pending' AND sp.publish_at <= now()
ORDER BY sp.publish_at
$$;
REVOKE ALL ON FUNCTION publishing_schedule_workspace(text) FROM PUBLIC;
REVOKE ALL ON FUNCTION publishing_due_schedules() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION publishing_schedule_workspace(text), publishing_due_schedules() TO sp_app;