-- 028: instance-level admin settings — content-policy posture for intake. -- -- The intake-posture decision record -- (docs/decisions/2026-10-09-intake-posture.md) grades the intake tiers by -- legal/ToS risk: -- -- YouTube channel RSS + metadata = safe (keyless, no quota, no ToS risk) -- YouTube full harvest (yt-dlp) = ToS risk — per-source opt-in ONLY -- (027's DEFAULT_MEDIA_POLICY already -- parks youtube_channel at 'metadata'); -- this switch is the instance-wide gate -- the per-source opt-in is subordinate to -- YouTube broadcast playlists = CC-only (Creative Commons licensed -- content only) — safe to rebroadcast -- Locked platforms (X/IG/TikTok) = metadata-only via sanctioned -- connectors; a bounded read-only pool is -- the decision record's Tier-2 "small -- bounded pool, later" option — opt-in -- -- Those posture choices are INSTANCE-level (one deployment = one collective, -- one egress IP, one legal exposure), not per-workspace: no workspace admin -- may widen them for everyone. Hence this table is a system table: -- -- * no workspace_id — deliberately. It is not a tenant row. -- * no RLS — there is nothing to isolate BY. Access control is at the API -- seam (SettingsService runs as sp_app; only Owner/Admin roles reach the -- setters through the controller's role check). NOT a grant-surface for -- tenant sessions: the app pool is the only grantee. -- -- key-value shape (key TEXT PK, value TEXT NOT NULL) per task spec: settings -- are few, admin-facing and read as a whole — typed decoding lives in -- apps/api/src/settings/settings.service.ts (the ONE place that interprets -- values; nothing else parses these strings), values are CHECK-constrained -- to the exact literal the service layer understands so a stray UPDATE can -- never park an undecodable row in the table. BEGIN; CREATE TABLE instance_settings ( key text PRIMARY KEY, value text NOT NULL, updated_at timestamptz NOT NULL DEFAULT now(), updated_by uuid REFERENCES users(id) ON DELETE SET NULL, -- Legal-exposure switches must never hold anything but the literals the -- service layer decodes; unknown keys are equally rejected so a typo can't -- fork the settings surface. CONSTRAINT instance_settings_key_allowed CHECK (key IN ( 'youtube_full_harvest', 'youtube_cc_only_broadcast', 'scrape_metadata_only', 'scrape_bounded_pilot', 'intake_posture' )), CONSTRAINT instance_settings_bool_value CHECK ( key NOT IN ('youtube_full_harvest', 'youtube_cc_only_broadcast', 'scrape_metadata_only', 'scrape_bounded_pilot') OR value IN ('true', 'false') ), CONSTRAINT instance_settings_posture_value CHECK ( key <> 'intake_posture' OR value IN ('metadata_only', 'community_submissions', 'bounded_pilot') ) ); -- --------------------------------------------------------------------------- -- Seed defaults (the safe posture — matches the 2026-10-09 decision record: -- "metadata-only + community submissions + Tier-1 sanctioned sources"). -- ON CONFLICT DO NOTHING keeps re-runs and later re-seeds idempotent. -- --------------------------------------------------------------------------- INSERT INTO instance_settings (key, value) VALUES ('youtube_full_harvest', 'false'), -- yt-dlp OFF at the gate ('youtube_cc_only_broadcast', 'true'), -- CC-only broadcast filter ('scrape_metadata_only', 'true'), -- locked platforms: metadata ('scrape_bounded_pilot', 'false'), -- no bounded X pool ('intake_posture', 'metadata_only') -- overall posture ON CONFLICT (key) DO NOTHING; -- System table: sp_app is the ONLY grantee (003's default privileges already -- cover it, but the explicit grant documents intent the way 006/008/009 do). GRANT SELECT, INSERT, UPDATE, DELETE ON instance_settings TO sp_app; COMMIT;