How to read Supabase code you did not write
Every earlier module in this course taught you to write schema, policies, client calls, edge functions, and realtime/storage wiring. This module teaches the other half: reading a migration, a supabase-js call, an edge function, or a config.toml someone — or something — else wrote, and deciding whether it is actually correct. An AI assistant hands you a table with a policy, a client that compiles, and a function that returns 200 on the first try, and that success is the trap: Postgres accepts the migration, tsc and deno check both pass, and the demo works — while the code silently grants nobody the access it meant to grant, or grants everybody the access it meant to withhold. Supabase’s own layers validate almost nothing about intent: a for select using (true) policy is syntactically perfect and semantically wrong, createClient() never rejects a call that will 403 at runtime, and security definer compiles identically whether or not search_path is pinned.
The single most common shape this takes, proven throughout this module’s catalog: code that works when tested with the service_role key and returns nothing — no error, 200 with an empty array — when tested with anon or a real user’s JWT. service_role bypasses Row Level Security entirely, so it makes a missing policy, a missing grant, or a table left out of a publication invisible during development. Read Supabase code in the order below specifically because each step is a place where “it worked when I tried it” and “it is correct” quietly diverge — and every lesson later in this course already carries its own ## Reading AI-generated code section drilling one layer of it in full depth.
flowchart TD
A["1. Schema & RLS<br/>tables, policies, using vs with check -- does every command have one?"] --> B["2. Roles & keys<br/>anon/authenticated/service_role -- which key is this code actually using?"]
B --> C["3. Auth flow<br/>getSession vs getUser, refresh rotation, custom claims -- verified or just decoded?"]
C --> D["4. Client calls<br/>{data, error} checked? singleton? frozen headers?"]
D --> E["5. Edge functions<br/>jsr:/npm: specifiers, which key initializes the client, secrets"]
E --> F["6. Realtime & storage<br/>publication membership, replica identity, folder-scoped policies"]
F --> G["7. Ops & pooling<br/>prepare:false, pg_net async, who actually has this key"]
1. Schema & RLS
Section titled “1. Schema & RLS”Before anything else, read the table definitions and every policy attached to them — because a table with RLS enabled and zero policies is not an error, it is a table that returns nothing to everyone except its owner, forever, with no warning at any layer.
- What to look for: does every table that needs one have
enable row level security? Does every command a client actually calls (select,insert,update,delete) have its own policy, or does the table only have aselectpolicy because that’s the first thing anyone tested? On anupdatepolicy, iswith checkwritten explicitly, or isusing (true)doing double duty as both — the exact shape that lets one user reassign another user’s row? Does asecurity definerRPC function pinset search_path = '', or does it trust the caller’s sessionsearch_pathfor its unqualified table references? - The question to ask: if this exact policy is deployed and a curious, technically-competent user of this app spoofs nothing at all — just changes a field the request already lets them send — what stops the write from landing on someone else’s row?
- Which lesson explains it: the AI Bug Catalog’s Schema & migrations and RLS & roles groups (entries 1–8) for the real
42501, the real hijacked row, and the real schema-injectedsecurity definerleak; Row Level Security Fundamentals and RLS Patterns and Pitfalls for both mechanisms proven in full.
2. Roles & keys
Section titled “2. Roles & keys”Once the schema and its policies are understood, check which of the three keys the code in front of you is actually using — because anon, authenticated, and service_role are not interchangeable, and service_role bypasses every policy from step 1 without saying so anywhere in the response.
- What to look for: is
SERVICE_ROLE_KEY(orSUPABASE_SERVICE_ROLE_KEY) referenced anywhere a browser bundle could ship it — a client component, a public repo, a.envfile sitting insidesupabase/functions/<name>/with no.localsuffix (the CLI’s own.gitignoreonly excludes.env.local/.env.*.local, not a plain.env)? Does a demo that “worked” useservice_rolespecifically because the actual RLS policy is broken and nobody noticed? - The question to ask: if this exact request is replayed with the
anonkey instead of whatever key the code actually used to test it, does it still return the data it claims to — or does the demo’s success depend entirely on a key that should never reach this code path? - Which lesson explains it: the AI Bug Catalog’s Ops & pooling group (entry 22) for the real side-by-side
anonvsservice_roleresponse; Securing the Data API and Project Setup and the Supabase CLI for the full role/grant model.
3. Auth flow
Section titled “3. Auth flow”Read every place a session or a claim is trusted — because getSession() decodes a JWT locally with no signature check, and code that treats its return value as verified identity is trusting whatever happens to be sitting in storage.
- What to look for: does a server-side authorization check call
getSession()(or readreq.cookiesand manually decode) instead ofgetUser()/getClaims()? Does a refresh-token retry loop assume every reuse is malicious, when a short, configurable grace window (refresh_token_reuse_interval) tolerates the immediately-previous token on purpose? Does a policy or a handler read a custom JWT claim (request.jwt.claims ->> 'user_role') that no Auth Hook in this project’sconfig.tomlwas ever written to add? - The question to ask: if the JWT this code is about to trust had its payload edited by hand — same header, same (now-invalid) signature bytes — does anything in this exact code path actually notice before the claims inside it are acted on?
- Which lesson explains it: the AI Bug Catalog’s Auth & sessions group (entries 9–11) for the real tampered-token proof and the real
refresh_token_already_used; Sessions and JWTs for both mechanisms in full depth.
4. Client calls
Section titled “4. Client calls”supabase-js never throws on a database or API error — it returns { data, error } — so code that only destructures data has silently deleted the one signal that tells you the query failed.
- What to look for: is
errorever checked, or does the code go straight todata.map(...)and crash onnullwith a confusingTypeErrorinstead of surfacing the realPGRST205/42501/whatever actually happened? Does branch logic key offerror.message(a string that can change between versions) instead of the stableerror.code? Is.single()called on a query that can return zero or several rows without aPGRST116handler? Is a newcreateClient()constructed per request or per component instead of one shared singleton? Wasglobal.headers.Authorizationset at construction time, silently overriding every laterauth.setSession()call for that client instance? - The question to ask: if this exact call fails — wrong table name, missing grant, expired session, anything — does the code that runs next behave any differently than it would on success, or does it just crash one line further down with a worse error than the one Postgres actually gave it?
- Which lesson explains it: the AI Bug Catalog’s Client calls group (entries 12–15) for the real crash-on-
null, the realPGRST116, the real singleton warning, and the real frozen-header proof; The supabase-js Client and Error Handling and Typed Queries for both mechanisms proven in full.
5. Edge functions
Section titled “5. Edge functions”An Edge Function runs on Deno, not Node — a bare import specifier that resolves fine in every other file in the same repository fails the moment it is deployed here, and the failure mode is a 503 with no indication in the code itself of what went wrong.
- What to look for: does an
importuse a bare specifier (@supabase/supabase-js) instead ofjsr:/npm:/a relative path? Which key doescreateClient()inside the function actually use —SUPABASE_ANON_KEY(subject to the exact same RLS as any other anon caller, proven in step 1) orSUPABASE_SERVICE_ROLE_KEY(the only one that behaves like “server-side code that bypasses RLS”)? Does a secret live in a plainsupabase/functions/<name>/.envinstead ofsupabase secrets set(hosted) or an explicitly.local-suffixed file (the only pattern this CLI’s own.gitignoreactually protects)? - The question to ask: does this exact file, checked with
deno checkagainst Deno’s own module graph — nottsc, not a Node type-checker — actually resolve every import it declares, and does the client it constructs carry the key this function’s job actually requires? - Which lesson explains it: the AI Bug Catalog’s Edge functions group (entries 16–18) for the real
BOOT_ERROR, the real anon-scoped[]from inside a function, and the real.gitignoregap; Writing and Invoking a Function and Edge Functions and the Database for both mechanisms in full.
6. Realtime & storage
Section titled “6. Realtime & storage”Both of these are ordinary Postgres underneath — a table’s rows (realtime.messages) and a policy-guarded table (storage.objects) — so the exact same schema/RLS reading habits from step 1 apply, with two extra, easy-to-miss prerequisites neither one advertises.
- What to look for: is the table a
postgres_changessubscription targets actually a member of thesupabase_realtimepublication — checkable, but not implied by the table simply existing? Isreplica identityfull, or does anUPDATE/DELETEpayload’soldsilently shrink to just the primary key (or vanish entirely)? Does a private channel have a matching policy onrealtime.messagesfor its exact topic, or does the subscription just fail withUnauthorized? Does a storage policy check(storage.foldername(name))[1]against the caller’s own id, or onlybucket_id— leaving every authenticated user free to write into every other user’s folder? Does anupsert: trueupload have a select policy in addition to insert/update — storage’s upsert path checks for the object’s existence first, and a missing select policy fails that check even when insert and update are both correctly scoped? - The question to ask: if this exact table/bucket/channel is queried directly against Postgres right now —
select * from pg_publication_tables,select relreplident,select policyname, qual from pg_policies— does what’s actually configured match what the code in front of you assumes is already true? - Which lesson explains it: the AI Bug Catalog’s Realtime and Storage groups (entries 19–24) for the real silent publication miss, the real private-channel rejection, the real
old: null, and the real cross-user folder write; Realtime Postgres Changes, Realtime Broadcast and Presence, and Storage Buckets and Policies for every mechanism proven in full.
7. Ops & pooling
Section titled “7. Ops & pooling”The last layer is everything about how a request reaches Postgres rather than what it asks for — connection pooling mode, background jobs, and which literal key ends up embedded where — and it is the layer most AI-generated code never mentions at all.
- What to look for: does code connecting through Supavisor’s transaction mode pooler assume named prepared statements work the way they do on a direct connection (
prepare: falseis the documented fix, not a style preference)? Does apg_netcall readnet._http_responsein the same statement that issued the request, assuming a synchronous round trip from what is, by design, an asynchronous background worker? Isdb pushrun against a pooled connection string withoutsslmode=disable, when this CLI’s own local pooler does not terminate TLS? - The question to ask: does this exact code’s correctness depend on the request completing synchronously, or on a session-scoped guarantee (a prepared statement, a
SET LOCAL) surviving past the one transaction Supavisor’s transaction mode actually guarantees it for? - Which lesson explains it: the AI Bug Catalog’s Ops & pooling group (entries 25–27) for the real
net._http_responserace and the documented (and, on this exact local stack, honestly non-reproduced) prepared-statement failure; Connection Pooling and Supavisor for the full session-vs-transaction-mode story.
What this module covers next
Section titled “What this module covers next”- The AI Bug Catalog — 27 specific patterns, each with the code AI typically writes, a real command and its real, freshly captured output against this module’s own local stack, and the fix.
- Review Checklist — the same 27 patterns as a one-page, eight-group checklist, each line linking the lesson that proves it.
- Verification Tools — what
supabase db lint, the RLS-coverage query,pg_policies,explain analyzeunder a real JWT,supabase test db(pgTAP),supabase gen types+tsc,deno check,curlwith anon vs service keys, the MCP server, Studio, and this course’s own harness each actually prove. - Every earlier lesson in this course already has its own
## Reading AI-generated codesection with a<SpotTheBug>— this module’s catalog does not repeat those, it groups a broader set of 27 patterns around the reading order above. Start from What Is Supabase if you want the full lesson-by-lesson path instead.
Mental Model
Section titled “Mental Model”Reading order for Supabase code you did not write:
1. Schema & RLS -> does every command (select/insert/update/delete) have its own policy?2. Roles & keys -> which of anon/authenticated/service_role is this code actually using?3. Auth flow -> getSession (decoded, unverified) or getUser (verified)?4. Client calls -> {data, error} checked? singleton? frozen Authorization header?5. Edge functions -> jsr:/npm: specifiers, which key, which secrets file6. Realtime/storage -> publication membership, replica identity, folder-scoped policies7. Ops & pooling -> prepare:false, pg_net is async, which key actually reached this code
"It returned data when I tested it" answers almost none of these -- the single most commonfailure in this catalog is code that works with service_role and is silently empty with anon.Common Misconceptions
Section titled “Common Misconceptions”- “If Postgres accepts the migration and
tsc/deno checkboth pass, the code is correct.” Proven throughout this module’s catalog:for update using (true)with no explicitwith checkis syntactically perfect SQL and a real row-hijack vulnerability, proven live in this course’s own RLS lessons — no compiler or linter catches it, because it is not a syntax error. - “A missing policy throws an error, so I’d notice.” Disproven directly: RLS enabled with zero policies returns
200with[]— no error at any layer, from Postgres up through PostgREST to the client. The silence is the bug. - “
service_roleis just a strongeranonkey.” It is a different thing entirely: it bypasses Row Level Security, which means every policy this module’s step 1 asks you to read is invisible while testing with it. Code that “just works” during development and breaks the moment a real user’s session replacesservice_roleis the single most common shape this catalog documents. - “AI-generated Supabase code just needs a newer prompt.” The problem is not staleness alone:
for select using (true)is real, current, syntactically correct SQL that simply does not mean what a reader who skips step 1 assumes it means — no training-data cutoff fixes a reading habit.