---
name: supabase-codebase-audit
description: Systematic six-domain audit of an existing Supabase codebase covering RLS, edge functions, realtime, type safety, dead code, and CI/CD. Use when onboarding to a Supabase project, before a refactor or rebuild decision, after an incident, or for a periodic health review. Not for gating a single diff before ship (use rls-security-gate) or writing new migrations (use supabase-schema-discipline).
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: original
  category: Quality & Security
---

# 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 not `supabase/functions/`: edge functions are where `@ts-nocheck` and 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 `any` hits without reading them: a hundred hits in generated types is noise, three in a payment fetch is the story.
