Supabase launch readiness
Supabase Auth and RLS Checklist for AI-Built Apps
A Supabase auth and RLS checklist for an AI-built app confirms, before launch, that row level security is on for every user-owned table, that each command has a policy checking the signed-in user rather than a client-side filter, that the service role key stays on the server, and that a second user cannot read the first user's rows. For an AI-built app on Vercel + Supabase, VibeRaven reads the migrations and client code in the repo and flags a public table created without RLS, RLS with no policies, policies that allow everyone with using (true), and a service role key in client code. It is advice, not a gate, and a repository check, not a live database or security test.
Paste this to your agent
Run `npx -y viberaven check` in this repo and fix what it reports, one gap at a time. Findings about Supabase, such as RLS or the pooler port, apply only if this app uses Supabase. Do not add Supabase or any other provider this app does not already use. Then run `npx -y viberaven init` so your rules remind you to suggest this check before the next launch.The check runs locally, with no sign-in and no API key. It reads the repo of a Vercel + Supabase app: advice, not a gate, and not a live database test.

Ready to try the policies with real users? How to test Supabase RLS policies.
Checklist
Check Supabase auth and RLS before launch
- 01Confirm RLS is enabled for user-owned and sensitive tables in the production project.
- 02Review policies for select, insert, update, and delete instead of relying on client filters.
- 03Make sure service-role keys stay server-side and never appear in client bundles or public env vars.
- 04Check auth session handling across server routes, API handlers, storage, and realtime paths.
- 05Verify migrations, seed data, backups, and production-vs-local project separation.
- 06Turn the highest-risk Supabase gap into one scoped coding-agent prompt with verification.
Reproducible example
One ownership rule, checked before and after the policy
Recorded 2026-10-31 against a disposable fixture, on Windows 11 with Node 24.12.0, viberaven 1.7.2 (the npm latest tag that day) and @electric-sql/pglite 0.5.8. The output below is copied from the terminal and trimmed: header lines, repeated findings and the long Assumptions and Not tested lists are cut, [...] lines are notes on what was cut, and long paths are shortened to ~/notes-app. Every other line is exactly as printed.
Create the disposable table
You need Node 20 or newer and a terminal in the project root; no account, no API key, nothing uploaded. Save this as supabase/migrations/20260928000000_notes.sql in a disposable local project. This is the exact table migration used for the recorded run below. It intentionally has no RLS yet; do not deploy this first step to a live project.
create table public.notes (
id uuid primary key default gen_random_uuid(),
user_id uuid not null default auth.uid() references auth.users (id) on delete cascade,
body text not null default '',
created_at timestamptz not null default now()
);Paste it into the free RLS checker first
Before installing anything, paste the migration into the free Supabase RLS checker on this site, or drop the file on it. It runs the same SQL rules as check, in your browser, and sends nothing anywhere. Checked on 2026-10-14, it showed one critical card for this migration, rls_disabled at supabase/migrations/20260928000000_notes.sql:1, the same rule id and title the terminal prints in Run 1, and after the correction below it said No findings in this SQL. It reads SQL only, so it cannot tell whether the policies keep the other user out; the rls-test matrix below is the step that tries that.
The rule
The fixture is a small notes app: a public.notes table with a user_id column, and a client page that calls supabase.from('notes').select() and trusts the database to return only the signed-in user's rows. The rule: a signed-in user may read, add, change and delete only their own notes. Another signed-in user and an anonymous request get nothing.
You write that rule yourself in viberaven.permissions.json at the project root. Never generate it from your own policies, or it will agree with every policy bug it is meant to catch.
{
"version": 1,
"description": "A signed-in user may only read, add, change and delete their own notes.",
"tables": {
"public.notes": {
"owner": { "column": "user_id", "is": "auth.uid()" },
"expect": {
"owner": { "select": "allow", "insert": "allow", "update": "allow", "delete": "allow" },
"other_user": { "select": "deny", "insert": "deny", "update": "deny", "delete": "deny" },
"anon": { "select": "deny", "insert": "deny", "update": "deny", "delete": "deny" },
"service_role": "excluded"
}
}
}
}The commands
check reads repo evidence and exits 1 on a blocker. rls-test is experimental: it applies supabase/migrations to an in-memory PGlite database and tries every cell of the matrix as owner, other_user and anon. It exits 0 when every tested cell passes, 1 when any cell fails, and 2 when nothing failed but something stayed Unknown.
PGlite is an optional add-on (about 25 MB), so rls-test needs the second package. Without it, npx -y viberaven@1.7.2 rls-test runs nothing and says so.
npx -y viberaven@1.7.2 check
npx -y -p viberaven@1.7.2 -p @electric-sql/pglite@0.5.8 viberaven rls-testRun 1: the table has no RLS
The only migration creates the table and nothing else. Expected: other_user and anon are denied all four actions. Actual: check exits 1 with one blocker, and rls-test exits 1 with 8 of 12 cells failing. In this local fixture, both the other_user and anon roles could read, add, change and delete the owner's note.
In 1.7.2, npx -y viberaven fix lists fix --gap rls_disabled for this project, marked as asking first, because enabling RLS without policies makes browser reads return no rows until policies exist. Here the migrations are named with a 14-digit timestamp, so that fix would write one new migration that enables RLS with no policies; this page writes the owner policies by hand instead, below.
Permissions caveat: the local runner supplies default role grants. In production, Data API access also depends on schema exposure and grants to the requesting role. RLS being off alone does not establish anonymous access. Supabase is changing its automatic public-schema grants, so inspect your actual project permissions. The quoted CLI warning below is the 1.7.2 output, not proof that a live project is exposed. See Supabase's explanation: https://supabase.com/docs/guides/api/securing-your-api.
$ npx -y viberaven@1.7.2 check
viberaven check · ~/notes-app
🔴 No RLS policy proof in migrations (rls_disabled)
1 public table without row level security: public.notes (supabase/migrations/20260928000000_notes.sql:1). The Data API does not enforce row restrictions for roles and operations allowed by the table's grants. Verify those grants to determine who can read or change rows.
🟡 No Content-Security-Policy header (missing_csp_header)
[...] 2 more warnings, 3 info
Verdict: ❌ 1 blocker, 3 warnings · score 45
$ echo $?
1
$ npx -y -p viberaven@1.7.2 -p @electric-sql/pglite@0.5.8 viberaven rls-test
viberaven rls-test (experimental) · ~/notes-app
PGlite imitation of Supabase, non-authoritative. Scenario passed locally does not prove production.
Migrations supabase/migrations: 1 file (20260928000000_notes.sql), sha256 fe7fb1edf1b55d60, applied
Engine PGlite 0.5.8 (PostgreSQL 18.3), in memory, network denied
Roles anon and authenticated: not superuser, not BYPASSRLS, not the table owner
public.notes (RLS off, owner postgres)
One row for the owner and one for another user (2 rows in the table).
actor action expected result observed
owner select allow Pass returned the owner row (sees 2 of 2 rows)
owner insert allow Pass 1 row added for the owner (2 to 3 rows)
owner update allow Pass 1 row changed (body rewritten; owner row changed)
owner delete allow Pass 1 row deleted, the owner row is gone (2 to 1 rows)
other_user select deny Fail returned the owner row (sees 2 of 2 rows)
other_user insert deny Fail 1 row added for the owner (2 to 3 rows)
other_user update deny Fail 1 row changed (body rewritten; owner row changed)
other_user delete deny Fail 1 row deleted, the owner row is gone (2 to 1 rows)
anon select deny Fail returned the owner row (sees 2 of 2 rows)
anon insert deny Fail 1 row added for the owner (2 to 3 rows)
anon update deny Fail 1 row changed (body rewritten; owner row changed)
anon delete deny Fail 1 row deleted, the owner row is gone (2 to 1 rows)
service_role all excluded N/A bypasses RLS by design, not tested
Result: 4 Pass, 8 Fail, 0 Unknown, 4 N/A · 12 of 12 tested cells observed (16 cells in the matrix)
Not ready within tested scope: 8 cells do not match the matrix; 0 unknowns.
$ echo $?
1The correction
One more migration turns RLS on and adds owner-only policies for the authenticated role. No policy mentions anon, so anon is denied by default once RLS is on.
-- supabase/migrations/20260928000100_notes_rls.sql
alter table public.notes enable row level security;
create policy "notes_select_own" on public.notes
for select to authenticated using ((select auth.uid()) = user_id);
create policy "notes_insert_own" on public.notes
for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "notes_update_own" on public.notes
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "notes_delete_own" on public.notes
for delete to authenticated using ((select auth.uid()) = user_id);Run 2: same commands after the policy
check now exits 0 with no blockers; the three unrelated warnings are still there. rls-test reports 10 Pass, 0 Fail, 2 Unknown and exits 2. The two Unknowns are the insert cells for other_user and anon: Postgres refused the generated row through WITH CHECK, but only that one row was tried, so the tool does not count it as a pass. That is the real result, not a clean sweep.
$ npx -y viberaven@1.7.2 check
viberaven check · ~/notes-app
🟡 No Content-Security-Policy header (missing_csp_header)
[...] 2 more warnings, 3 info
Verdict: ✅ no blockers, 3 warnings · score 65
$ echo $?
0
$ npx -y -p viberaven@1.7.2 -p @electric-sql/pglite@0.5.8 viberaven rls-test
viberaven rls-test (experimental) · ~/notes-app
PGlite imitation of Supabase, non-authoritative. Scenario passed locally does not prove production.
Migrations supabase/migrations: 2 files (20260928000000_notes.sql, 20260928000100_notes_rls.sql), sha256 32631a08e2ff4f80, applied
Engine PGlite 0.5.8 (PostgreSQL 18.3), in memory, network denied
public.notes (RLS on, owner postgres)
One row for the owner and one for another user (2 rows in the table).
actor action expected result observed
owner select allow Pass returned the owner row (sees 1 of 2 rows)
owner insert allow Pass 1 row added for the owner (2 to 3 rows)
owner update allow Pass 1 row changed (body rewritten; owner row changed)
owner delete allow Pass 1 row deleted, the owner row is gone (2 to 1 rows)
other_user select deny Pass returned no row (sees 1 of 2 rows)
other_user insert deny Unknown refused by WITH CHECK: new row violates row-level security policy for table "notes" (42501). Only the generated row was tried; other values may allow insertion
other_user update deny Pass 0 rows changed, the row is still there and unchanged
other_user delete deny Pass 0 rows changed, the row is still there and unchanged
anon select deny Pass returned no row (sees 0 of 2 rows)
anon insert deny Unknown refused by WITH CHECK: new row violates row-level security policy for table "notes" (42501). Only the generated row was tried; other values may allow insertion
anon update deny Pass 0 rows changed, the row is still there and unchanged
anon delete deny Pass 0 rows changed, the row is still there and unchanged
service_role all excluded N/A bypasses RLS by design, not tested
Result: 10 Pass, 0 Fail, 2 Unknown, 4 N/A · 10 of 12 tested cells observed (16 cells in the matrix)
No blockers found within tested scope; 2 unknowns.
[...] the Unknown note and the Assumptions and Not tested lists
PGlite imitation of Supabase, non-authoritative. Scenario passed locally does not prove production.
$ echo $?
2What these runs do not prove
Both runs read the repo. rls-test runs your migrations in PGlite, an imitation of Supabase: Supabase Auth and the Data API are not run, auth.uid() comes from a JWT claim the test sets, and the live project is never contacted. So this repo and PGlite evidence does not establish the production database's active RLS configuration. Production can differ: migrations applied out of order or not at all, policies edited in the dashboard, different default grants.
Confirm it on the live project itself: look at the table and its policies in the Supabase dashboard, or query pg_class.relrowsecurity and pg_policies with read-only access.
Next step from your coding agent (MCP)
Prerequisites: Node 20 or newer, an MCP client such as Claude Code, Codex or Cursor, and the project root as the working directory (or pass it as cwd). The server is npx -y @viberaven/mcp over stdio. Add it to your MCP config as below, then ask the agent to call viberaven_check_readiness. Full setup is in /mcp.md.
On the fixed fixture, @viberaven/mcp 1.7.2 listed 27 tools, and viberaven_check_readiness returned score 65, 0 blockers and 3 warnings, the same verdict as the terminal check. It also lists a viberaven_rls_test tool, but started with npx it returned an error asking for an installed CLI with PGlite next to it, so run the PGlite command above in a terminal for the matrix.
{ "viberaven": { "command": "npx", "args": ["-y", "@viberaven/mcp"] } }Supported limits in 1.7.2
- Ownership is one column equal to auth.uid(). Policies that look up teams, organisations or memberships only see the rows your migrations create; no membership rows are seeded.
- Actors are owner, other_user and anon. service_role bypasses RLS and is always excluded.
- WITH CHECK is tried with the generated row only. A refusal is reported as Unknown, not as a pass.
- Storage, Realtime and Edge Functions are not modelled. Views, functions and RPC calls are only reached through the listed tables. supabase/seed.sql is not applied.
- check looks for RLS evidence in migrations. Whether the policies do what you meant is what the rls-test matrix is for.
When this is not enough
If your access rules depend on team membership, shared rows, storage buckets, RPC functions or security definer views, a single-owner matrix will miss the real risk. The same goes for a live project that has drifted from the repo. A clean local run means your migrations match the rule you wrote, inside an imitation, and nothing more. Before real data goes in, sign in as two real users against a Supabase branch or staging project and try to read each other's rows, check pg_policies on production, and have someone who knows Postgres RLS review the policies.
Related
Related guides
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
What should I check before launching a Supabase app?
Check RLS, policies, auth session handling, server-only service role key usage, migrations, storage access, backups and production environment separation. VibeRaven covers the RLS, policy and service role key parts from the repo.
Can VibeRaven prove RLS is enabled in Supabase?
No. It shows migration and code evidence. The live database can differ, so confirm it in the Supabase dashboard, for example with the Security Advisor.
Why is client-side filtering not enough?
Client-side filtering can hide data in the UI but does not reliably enforce access on the server or database. Sensitive records need authorization and database policies.