The Next.js + Supabase Security Checklist
The short version: most Next.js + Supabase breaches trace back to four predictable places — a table without RLS, a leaked key, an unprotected server entry point, and an unpatched dependency. Work through the checklist below before you ship and you close the gaps that actually get exploited. The most common failure — a Supabase table reachable by anyone holding the public anon key — was disclosed at scale in CVE-2025-48757, where researcher Matt Palmer found Supabase tables in production apps leaking emails, addresses, and API keys because Row Level Security wasn't protecting them.
This is the master checklist. Each item is one thing to do, one sentence on why it matters, and a link to the deep-dive when you want the detail. Run it before launch, and re-run it before any release that touches auth, data access, or environment variables.
What's the security checklist for a Next.js + Supabase app?
The checklist breaks into four areas: your Supabase database and RLS, your secrets and keys, your Next.js application layer, and your dependencies. Get all four right and you've covered the failure modes behind nearly every Next.js + Supabase incident. Here they are in order of blast radius.
1. Supabase database & Row Level Security
Your Postgres public schema is exposed through Supabase's auto-generated PostgREST API, reachable with the anon key that ships in every browser bundle. RLS is the only thing standing between an anonymous request and your rows.
- Enable RLS on every table in
public. A table without RLS is fully readable — and often writable — by anyone with your anon key, which is public by design. This is the single most exploited Supabase mistake; see tables without RLS for how to find every unprotected one. - Never
DISABLE ROW LEVEL SECURITYto "fix" a query. Disabling RLS doesn't grant a permission, it removes the requirement to have one — turning a private table into an open API. The real fix is almost always a missing policy, not a disabled gate (why disabling RLS is catastrophic). - Scope every policy to the user, not to everyone. A policy is only as good as its
USINGclause; one that forgetsauth.uid() = user_idlets every authenticated user read every other user's rows (the not-user-scoped policy trap). - Audit any policy that uses
using (true).truemeans "always pass" — fine for a deliberately public read, catastrophic on a table that should be private or writable (theusing (true)trap). - Parameterize SQL in RPCs and Edge Functions. String-concatenated SQL inside a
SECURITY DEFINERfunction runs with elevated rights and is a direct injection path (Supabase SQL injection in Next.js). - Lock down public storage buckets. A bucket set to public serves every object to anyone who can guess the URL pattern — including files you assumed were private (public storage bucket leaks).
- Require auth in Edge Functions. Edge Functions don't inherit your RLS context automatically; an unauthenticated function holding the service role is a backdoor around every policy (Supabase Edge Function auth).
2. Secrets & keys
The fastest way to undo perfect RLS is to leak the key that bypasses it. Treat every key as either "safe in the browser" or "server-only," and never confuse the two.
- Keep the
service_rolekey server-only — neverNEXT_PUBLIC_. The service_role key bypasses every RLS policy; in the browser it's a full database compromise, not a misconfiguration (service role key exposed). - Understand what
NEXT_PUBLIC_actually does. Any variable with that prefix is inlined into the client bundle at build time and shipped to every visitor — so it must only ever hold values you'd publish on a billboard (howNEXT_PUBLIC_leaks keys). - Never hardcode keys or JWTs in source. A literal
eyJ…token or API key in your code lives in git history forever, even after you delete it — and bots scrape public repos for exactly that (hardcoded API keys in Next.js, hardcoded service_role JWT). - Add
server-onlyto modules that touch secrets. Theserver-onlypackage turns an accidental client import of a secret-holding module into a build-time error instead of a silent leak. - Rotate any key you suspect was exposed. A key that hit a bundle or a public commit must be treated as burned; Supabase's rotation guide walks through replacing it, after which you purge it from git history.
3. Next.js application layer
RLS protects your data; the app layer protects everything in front of it. Server Actions and Route Handlers are public HTTP endpoints — they don't get authenticated just because the UI that calls them is behind a login.
- Check auth at the top of every Server Action. A Server Action is a callable POST endpoint; without an explicit session check, anyone can invoke it directly and skip your UI entirely (Server Action auth checks).
- Validate and type every API route input. Trusting the request body lets a caller send fields you never expose in the form — including ones that change ownership or roles (API route input validation).
- Set a real
matcheron your middleware. A middleware that doesn't match the routes you think it does silently stops protecting them; verify the matcher actually covers your guarded paths (middleware matcher mistakes). - Never reflect the request origin into CORS. A wildcard or echoed
Access-Control-Allow-Originlets any site make credentialed calls to your API (CORS wildcard in Next.js). - Don't pass untrusted HTML to
dangerouslySetInnerHTML. It does exactly what the name says — renders attacker-controlled markup as live DOM, the classic stored-XSS vector (dangerouslySetInnerHTMLXSS). - Never
evaluser input or build dynamic code from it.evaland its cousins (new Function,setTimeout("string")) turn any string you don't fully control into executable code (whyevalis dangerous).
4. Dependencies
You ship your dependencies' code as if it were your own. A vulnerable package in node_modules is a vulnerability in your app, regardless of how clean your own code is.
- Run
npm auditand patch known CVEs before launch. A single transitive dependency with a known RCE or prototype-pollution flaw undermines everything else on this list (vulnerable npm dependencies). - Pin and review what you pull in. Lockfiles make builds reproducible, but they don't make a package safe — review new dependencies and keep the tree as small as you can defend.
How do I run this checklist on every push instead of by hand?
You automate it. A pre-launch audit catches today's gaps, but the table someone adds next sprint, the key a teammate names NEXT_PUBLIC_ next month, and the CVE published next week won't show up until you look again. A checklist you run by hand only protects you on the days you remember to run it.
That's the job GuardLayer does: it scans your Next.js + Supabase code on every push, comments the exact issues inline on the PR, and posts a merge-gate Check Run so an unprotected table or a leaked service_role key can't reach main. Every item in this checklist maps to a rule it enforces continuously.
You can run a free scan on your repo right now and see which of these the engine flags — no setup, just the same checklist applied to your actual code.
FAQ
What's the most important item on a Next.js + Supabase security checklist?
Enabling RLS on every table in your public schema. It's the most common real-world failure — CVE-2025-48757 exposed it at scale across production apps — and without it your anon key, which is public by design, can read and often write every row.
Is the Supabase anon key safe to expose? Yes, by design — it's constrained by your RLS policies and is meant to ship in the browser. The service_role key is the opposite: it bypasses RLS entirely and must stay server-side. Confusing the two is the root of most key-leak incidents.
Do I still need app-layer auth checks if RLS is on? Yes. RLS protects database rows, but Server Actions and API routes are public endpoints that can run logic, call external services, or use the service_role key. Each needs its own session check; RLS doesn't cover code that runs before or around the query.
How often should I run through this checklist? Before every launch, and before any release that touches authentication, data access, or environment variables. Better still, automate the checks so they run on every push rather than depending on memory.
Does enabling RLS with no policies break my app?
It locks the table — RLS is default-deny, so with no policy zero rows return. That's the safe state. Add a policy that grants exactly the access your query needs, usually using ( (select auth.uid()) = user_id ), rather than disabling RLS.
Catch this before it ships — free
GuardLayer scans every push for this and 19 other Next.js + Supabase issues, with the exact fix inline.