-- 037: per-workspace chat identity display settings (pseudonymization). -- -- Owner need, two parts: -- * Hub-local users must render as @username (never @hub_user:server) on the -- surfaces the HUB renders — that part is code-only (mxid-display.ts). -- * Per-collective pseudonymization: participants in PUBLIC chats appear -- under automatically-assigned random display names; PRIVATE chats show -- real names by default; an admin toggle decides whether participants can -- see the real→pseudonym mapping. -- -- STORAGE DECISION (why a new table and not instance_settings / branding): -- * instance_settings (028) is the INSTANCE's legal/content posture — a -- SYSTEM table with no workspace_id and a CHECK constraint pinning the key -- set to exactly its five policy keys. chat.* keys cannot live there, and -- pseudonymization is a PER-COLLECTIVE choice, not a deployment posture. -- * workspaces.branding (036) is the tenant's LOOK — reusing it for chat -- policy would couple two unrelated concerns inside another module's column. -- So nothing that exists fits; this table is the per-workspace key/value -- analogue of 028, following its conventions: the key set and the value -- literals are CHECK-constrained so an undecodable row cannot exist, and the -- ONE place that interprets the strings is chat-display.ts (missing/unknown -- rows decode to the documented default). -- -- Keys and defaults (decoded in apps/api/src/matrix-sso/chat-display.ts): -- chat.pseudonymizePublic default true public chat: random display names -- chat.pseudonymizePrivate default false private chat: real names -- chat.revealMapping default false participants see Pseudonym (real) -- No seed rows: absence IS the default, which keeps the table empty for every -- workspace that never touches the toggles (the `pending` semantics of 028). BEGIN; CREATE TABLE workspace_chat_settings ( workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT, key text NOT NULL, value text NOT NULL, updated_at timestamptz NOT NULL DEFAULT now(), updated_by uuid REFERENCES users(id) ON DELETE SET NULL, PRIMARY KEY (workspace_id, key), -- Unknown keys are rejected outright so a typo cannot fork the surface. CONSTRAINT workspace_chat_settings_key_allowed CHECK (key IN ( 'chat.pseudonymizePublic', 'chat.pseudonymizePrivate', 'chat.revealMapping' )), -- Every key is boolean-valued; only the literals the service layer decodes. CONSTRAINT workspace_chat_settings_bool_value CHECK (value IN ('true', 'false')) ); CREATE TRIGGER trg_workspace_chat_settings_ws_immutable BEFORE UPDATE ON workspace_chat_settings FOR EACH ROW EXECUTE FUNCTION reject_workspace_change(); ALTER TABLE workspace_chat_settings ENABLE ROW LEVEL SECURITY; ALTER TABLE workspace_chat_settings FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON workspace_chat_settings 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 workspace_chat_settings TO sp_app; COMMIT;