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.ts asserts 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.