-- Append-only audit log. One database == one organization, so there is no org_id. CREATE TABLE audit_log ( seq BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, at TIMESTAMPTZ NOT NULL DEFAULT now(), actor_kind TEXT NOT NULL CHECK (actor_kind IN ('user', 'system')), actor_id UUID, action TEXT NOT NULL CHECK (action IN ('created', 'updated', 'deleted')), entity_type TEXT NOT NULL, entity_id UUID NOT NULL, changes JSONB NOT NULL DEFAULT '[]'::jsonb, CONSTRAINT actor_id_matches_kind CHECK ( (actor_kind = 'user' AND actor_id IS NOT NULL) OR (actor_kind = 'system' AND actor_id IS NULL) ) ); CREATE INDEX audit_log_entity_idx ON audit_log (entity_type, entity_id, seq); -- Enforce append-only at the database level: reject any UPDATE or DELETE. CREATE OR REPLACE FUNCTION audit_log_reject_mutation() RETURNS trigger AS $$ BEGIN RAISE EXCEPTION 'audit_log is append-only; % is not permitted', TG_OP; END; $$ LANGUAGE plpgsql; CREATE TRIGGER audit_log_immutable BEFORE UPDATE OR DELETE ON audit_log FOR EACH ROW EXECUTE FUNCTION audit_log_reject_mutation();