Security reference
Row Level Security policies
Every table in the app database is protected by Row Level Security: with RLS enabled and no matching policy, a request is denied by default. The three policies below close the findings reported by the security scanner and are re-verified on every pull request.
audit_logs_missing_insert_policy
audit_logs · INSERT
audit_logs_self_insert
Signed-in users may only write audit rows attributed to themselves; anonymous inserts and spoofed user_id values are rejected.
CREATE POLICY "audit_logs_self_insert"
ON public.audit_logs FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);document_exports_missing_update_policy
document_exports · UPDATE
document_exports_no_update
Export records are immutable: the UPDATE policy evaluates to false for both USING and WITH CHECK, so no client can rewrite an export's storage path or owner.
CREATE POLICY "document_exports_no_update"
ON public.document_exports FOR UPDATE TO authenticated
USING (false) WITH CHECK (false);user_roles_admin_insert_escalation
user_roles · INSERT
user_roles_admin_insert
Only admins may grant roles, never to themselves, and never the admin role — blocking both self-escalation and admin cloning through the Data API.
CREATE POLICY "user_roles_admin_insert"
ON public.user_roles FOR INSERT
WITH CHECK (
has_role(auth.uid(), 'admin'::app_role)
AND user_id <> auth.uid()
AND role <> 'admin'::app_role
);How these are enforced
- Manifest: the policies are declared in
src/lib/security/policy-manifest.ts, the single source of truth shared by docs, tests and CI. - Tests:
tests/rls-policies.test.tsasserts each policy exists in a migration with its ownership and escalation guards intact, and — when database credentials are available — that anonymous writes are rejected. - CI gate: the security workflow runs those tests, scans migrations for anonymous grants and tables created without RLS, and audits dependencies. A regression fails the check and blocks merging.