SKILL.md
Supabase Codebase Audit
Audit with commands, not impressions. Every finding needs evidence you can paste: a grep hit or a SQL result. No evidence, no finding.
When to run
- Onboarding to an existing Supabase project.
- Before a major refactor or a build-vs-rebuild decision.
- After a security incident.
- Periodic security and health review.
The six domains
Work them in order. Read references/audit-commands.md for the exact grep and SQL per domain, run them verbatim, and paste outputs as evidence.
| # | Domain | What it surfaces | Highest-severity signal |
|---|---|---|---|
| 1 | RLS policies | Permissive or missing policies | USING (true) on a PII table: CRITICAL |
| 2 | Edge function health | Monoliths, type escapes, open endpoints | verify_jwt = false without documented justification |
| 3 | Realtime publications | WAL decode waste, dead subscriptions | Published table with zero frontend subscribers |
| 4 | Type safety | as any, : any, @ts-nocheck census | as any in data-fetching code: data integrity risk |
| 5 | Dead code and triggers | Orphan cron jobs, duplicate triggers | Cron job with zero successes |
| 6 | Deployment and CI/CD | Missing pipelines, committed secrets | Live secret key in the repo: CRITICAL, rotate now |
Domain notes:
- Domain 1: cross-check three ways: policies containing
true, tables with no policy at all, tables with RLS disabled. A table passing one check can fail another. - Domain 2: functions over 500 lines are monolith candidates. Count
@ts-nocheck,as any, and raw console logging per function. - Domain 3: the waste runs both directions. Published tables nobody subscribes to burn WAL decoding. Frontend subscriptions to unpublished tables are dead code that looks alive.
- Domain 4: prioritize fixes in data-fetching code first, then handlers, then UI.
- Domain 5: red flags are duplicate triggers on one table, triggers missing WHEN clauses, and scheduled jobs that never succeed.
- Domain 6: any hit on a live secret pattern is an immediate stop: rotate the key before continuing the audit.
Output format
One table, sorted by severity:
| Finding | Severity | Evidence | Recommended fix |
|---|---|---|---|
| <description> | CRITICAL / HIGH / MEDIUM / LOW | <grep output or SQL result> | <specific fix> |
Severity rules: CRITICAL means exploitable now or data loss in progress. HIGH means exploitable with effort or silent corruption. MEDIUM means debt with a trigger condition. LOW means hygiene.
Good vs bad
Good finding: "USING (true) SELECT policy on public.profiles (migrations/0042.sql:18), table holds emails. CRITICAL. Fix: replace with auth.uid() = user_id, add admin path via has_role."
Bad finding: "RLS coverage seems inconsistent and could be improved." No table, no file, no severity, not actionable. Delete findings like this, they pad the report and bury the real ones.
Verification
Do this: after writing the report, re-run the Domain 1 commands from references/audit-commands.md. Expect every CRITICAL and HIGH RLS finding in the report to correspond to a current command output, and every command output row to appear in the report. If a finding has no reproducing command output, cut it. If an output row has no finding, the audit is incomplete, add it.
Completion checklist
- [ ] All six domains executed with commands, none skipped
- [ ] Every finding carries pasted evidence
- [ ] Severity assigned by the rules above, not by feel
- [ ] Any committed secret escalated for rotation immediately, not just reported
- [ ] Report sorted by severity with a specific fix per finding
Any box unchecked: not done. Fix or say so.
Footguns
- Auditing migrations only: migrations show intent, the live database shows truth. Run the SQL against the live schema, policies drift after hotfixes applied in the dashboard.
- Grepping
src/but notsupabase/functions/: edge functions are where@ts-nocheckand secrets hide most often. - Reporting a committed secret without rotating it: the report itself now documents a live credential. Rotate first, then write it up.
- Counting
anyhits without reading them: a hundred hits in generated types is noise, three in a payment fetch is the story.