Supabase Security Review Skill for Antigravity CLI
Review a Supabase change for RLS gaps, service-role keys in clients, and auth that trusts a caller-supplied user id. Use for a Supabase security review. Do not use for a non-Supabase repository or to apply SQL against production.
name: "supabase-security-review" description: "Review a Supabase change for RLS gaps, service-role keys in clients, and auth that trusts a caller-supplied user id. Use for a Supabase security review. Do not use for a non-Supabase repository or to apply SQL against production."
Supabase Security Review
Shared skill supabase-security-review for Supabase. Codex and Antigravity CLI both read .agents/skills/supabase-security-review/SKILL.md. Install this one file. Codex calls it with $supabase-security-review or /skills. Cursor, GitHub Copilot, and Devin also read .agents/skills. A second copy with the same name collides. Do not install another tool's file there.
When to run
Run this review for a Supabase change: a new table, a policy, a client call, or a request to "check auth" or "review RLS".
When to leave it
Do not upgrade dependencies or the framework version unless the user asked.
Stop for a repository that does not use Supabase. Do not run SQL against a hosted production database. Do not print keys.
Steps
- Read the installed
@supabase/supabase-jsversion. Note where the browser client and the server client are constructed. - Search the diff for
service_role,sb_secret_, and a database connection string. A hit in client code or a committed env file is a high finding. A publishable oranonkey in the browser, paired with RLS, is not a finding. - For each new or changed table, confirm row level security is on. Read policies by command. An
INSERTwithoutWITH CHECK, or aDELETEthat usesWITH CHECK, is a high finding. AnUPDATEthat setsUSINGand omitsWITH CHECKis valid: Postgres appliesUSINGto the new row. ADELETEpolicy ofUSING ((select auth.uid()) = user_id)is valid. Do not flag either. Prefer thatselectwrapper. A bareauth.uid()still runs, once per row. A policy ofusing (true)on private data is a high finding. - For each client write, confirm the user and tenant come from the session. A body field named
user_idortenant_idthat is stored is a high finding. - For each realtime subscription, confirm cleanup. A filter is not a security control. Missing RLS or Realtime authorization on a private table is a high finding. A payload that includes a token is a high finding.
- Look for a test or a local reproduction with two users. Missing coverage on a new policy is a medium finding. Do not apply a migration to production to "see if it works".
Output
Findings with severity, file, line, the policy or key path, and a fix. End with no file edits unless a fix was requested, and never include a secret in the report.
## Findings
- high | path:line | what is wrong | what to do instead
## No findings
Say "No findings" and list the files you opened. A search you skipped is not a clean result.
## Example
Input: `policies.sql` adds `create policy notes_delete on notes for delete using ((select auth.uid()) = user_id)` with no `WITH CHECK`.
Output: No findings. A DELETE policy cannot have `WITH CHECK`. Flag a `service_role` client imported from a browser file.
Do not flag the valid pattern named in the standards. Flag the mistake next to it.
What to check
- Keys:
service_roleorsb_secret_in browser code. A publishable oranonkey with RLS on is not a finding. - RLS: a table with no policy, or a policy that trusts a client-supplied user id.
- Policy shape:
WITH CHECKonDELETEorSELECT.UPDATEmay omitWITH CHECKand reuseUSING. - Realtime: a client filter treated as authorization.
- Secrets: the secret key committed or logged.
Stop
Stop when the user asks you to disable RLS, to use the service role in the browser, or to run against production.
Severity
high is a bug a caller can hit or a secret that leaks. medium is a broken contract that needs a specific input. low is a cleanup that does not change behaviour.