← All posts
·13 min read·GuardLayer

Supabase Security Checklist for Next.js (2026)

SupabaseNext.jsRLSSecurityChecklist

To secure a Next.js + Supabase app before launch: turn on RLS with user-scoped policies for every table in public, grant the Data API roles only what each table needs (new tables get no grants since October 30, 2026), keep secret and service_role keys out of anything prefixed NEXT_PUBLIC_, verify the user with getClaims() or getUser() inside every Server Action and route handler, switch on leaked-password protection, and run a Next.js version at or above the current security floor. The six sections below give each check, why it matters and the exact fix.

The best-known incident on this stack is CVE-2025-48757, where researcher Matt Palmer found production apps whose Supabase tables leaked emails, addresses and API keys to anyone holding the public key, because Row Level Security wasn't protecting them.

Last checked: 2026-11-03, against the Supabase docs, Supabase's GitHub discussions and the Next.js docs and blog. Run the list before launch, and again before any release that touches auth, data access or environment variables.

How do I secure a Next.js and Supabase app before launch?

You secure it in two layers: the database has to refuse anything RLS doesn't allow, and your Next.js server code has to authenticate and authorize every request before it touches data, because Server Actions and route handlers are public HTTP endpoints. Keys, Auth settings and patched dependencies keep those two layers intact.

Four things changed in 2026 that older checklists miss:

  • New tables aren't exposed by default. Since October 30, 2026, existing Supabase projects stop auto-granting select, insert, update and delete on new public tables to anon, authenticated and service_role (discussion #45329). The risky fix is a blanket grant ... to anon.
  • The legacy keys are on their way out. Supabase's API keys docs say it "is deprecating the anon and service_role keys by the end of 2026."
  • middleware.ts is now proxy.ts in Next.js 16, and Supabase's Next.js auth guide now says to protect pages with getClaims().
  • Next.js ships security releases every few weeks. Three of the five since July 2026 fixed critical remote code execution bugs.

1. Database and Row Level Security

Supabase exposes your public schema through its Data API, which anyone can call with the publishable (or legacy anon) key that ships in your client bundle. RLS and grants are what stand between that key and your rows.

  • RLS is on for every table in public. A table without RLS is readable, and often writable, by anyone with your public key. Fix: alter table public.<t> enable row level security; in the same migration that creates it. Find the stragglers with tables without RLS.
  • Nobody "fixed" a query by disabling RLS. disable row level security doesn't grant a permission, it removes the gate. If a query returns nothing, a policy is missing (why disabling RLS is catastrophic).
  • New tables get explicit, minimal grants. After October 30, a table you create isn't reachable through the Data API until you grant it. Grant per role and per privilege, next to enable row level security, never grant all ... to anon on a table without RLS (the October 30 grant change).
  • Every policy is scoped to the user. A policy that forgets (select auth.uid()) = user_id lets every signed-in user read everyone's rows. (the not-user-scoped policy trap).
  • using (true) appears only on data that's public on purpose. It means "always pass", which is fine for a public read and a breach on anything private or writable (the using (true) trap).
  • No policy reads user_metadata. Supabase's RLS guide says raw_user_meta_data "can be updated by the authenticated user", so a role stored there is self-service. Put roles in app_metadata (user_metadata vs app_metadata).
  • Views use security_invoker. The same guide says "Views bypass RLS by default because they are usually created with the postgres user." Fix: alter view public.<v> set (security_invoker = on); (the Security Definer View fix).
  • SECURITY DEFINER functions aren't callable by everyone. Postgres grants EXECUTE on every new function to PUBLIC, so anon can call it over /rest/v1/rpc, and RLS doesn't apply inside it. Fix: revoke execute on function public.<fn>(<args>) from public, anon, authenticated; then grant back only who needs it (who can execute your SECURITY DEFINER functions).
  • Those functions pin search_path. Add set search_path = '' and schema-qualify every name inside, or a caller can shadow your tables (Function Search Path Mutable).
  • SQL in RPCs and Edge Functions is parameterized. String-built SQL inside a definer function runs with the owner's rights (SQL injection in Supabase + Next.js).
  • Realtime doesn't carry sensitive deletes. Supabase's production security checklist notes that RLS doesn't filter Postgres Changes DELETE events. Use Broadcast with Realtime Authorization for sensitive data (Realtime and RLS).
  • Storage buckets are private unless every file is public. A public bucket serves any object to anyone with its URL. Use private buckets and short-lived signed URLs (public bucket leaks, signed URL security).
  • Edge Functions authorize the caller in code. A function holding the secret key with no auth check is a way around every policy (Edge Function auth).
  • Your ORM isn't quietly skipping RLS. Prisma and Drizzle connect as postgres, which owns your tables, so policies don't run on those queries. Keep the auth check in code, or connect as a non-owner role (ORMs bypass RLS).
  • You've tested the policies as real users. Run each query as anon and as two different users before launch (how to test Supabase RLS).

2. Supabase Auth settings

These live in the dashboard, not in your repo, so code review never sees them.

  • Leaked-password protection is on. It checks new passwords against HaveIBeenPwned and rejects breached ones. It's off by default and needs the Pro plan or above, per Supabase's password security docs. Fix: Authentication → Sign In / Providers → Email → "Prevent use of leaked passwords" (where to enable it).
  • Passwords have a real minimum. The same page advises against a minimum length below 8 characters, and you can require digits, lowercase, uppercase and symbols.
  • Email confirmations are on, and OTP expiry is 3600 seconds or less. Both come from Supabase's production security checklist. The expiry also covers confirmation, recovery, email-change and invite links (expired email links).
  • Auth email comes from your own SMTP. Custom SMTP sends from your domain and lifts the low default email rate limit (Auth rate limits).
  • Signup, sign-in and password reset have CAPTCHA if bots are a concern. Supabase supports it on all three endpoints.
  • Anonymous sign-ins are off unless you use them. Anonymous users get the authenticated role, so policies written for "signed-in users" let them in too (anonymous sign-ins).
  • Test phone numbers are removed before launch.

3. API keys and secrets

Perfect RLS is undone by leaking the one key that skips it. Every key is either safe in the browser or server-only.

  • The secret key and service_role are server-only. Supabase's docs: "A secret key bypasses every Row Level Security policy you have." It never goes in a NEXT_PUBLIC_ variable or a "use client" file (service role key exposed).
  • You know what NEXT_PUBLIC_ does. Next.js inlines those variables into the client bundle at build time, so they reach every visitor (how NEXT_PUBLIC_ leaks keys).
  • You've moved to sb_publishable_ and sb_secret_ keys. Supabase's timetable in discussion #29260 says new projects stopped getting anon and service_role in November 2025 and lists their removal as "Late 2026, TBC" (publishable vs secret vs legacy keys).
  • Every client is migrated before you deactivate the legacy pair. Cron jobs, Edge Functions and CI scripts that still send the old JWT start failing with Legacy API keys are disabled.
  • Don't count on Supabase's browser block. Supabase returns 401 for a secret key sent with a browser User-Agent, but a key in your bundle still works from any script. If you've seen Forbidden use of secret API key in browser, rotate it.
  • No keys or JWTs are hardcoded. A literal eyJ… or sb_secret_… in source lives in git history after you delete it (hardcoded API keys, hardcoded service_role JWT).
  • Secret-holding modules start with import 'server-only'. Next.js turns an accidental client import into a build error instead of a leak (use server vs server-only).
  • AI agents don't hold the service_role key. Run the Supabase MCP server in read-only mode with a scoped token (the MCP lethal trifecta, MCP configs leaking secrets).
  • Exposed keys are rotated, not just deleted. Follow Supabase's rotation guide, then purge the key from git history.

4. Next.js app layer

RLS protects rows; the app layer protects everything in front of them. The Next.js data security guide is explicit: "A page-level authentication check does not extend to the Server Actions defined within it."

  • Every Server Action checks auth and ownership itself. Next.js says to "treat Server Actions as reachable via direct POST requests and verify authentication and authorization inside each one." Check who the user is, then that they own the record (the Server Action auth check).
  • Server code verifies the user with getClaims() or getUser(), never getSession(). Supabase: "Never trust supabase.auth.getSession() inside server code such as Proxy", because it reads the cookie without revalidating it (getSession vs getUser vs getClaims).
  • proxy.ts is a redirect, not your authorization boundary. Next.js 16 renamed middleware.ts to proxy.ts, and Supabase notes a proxy.ts is never called on Next.js 15 and earlier. Re-check auth where the data is read (where auth checks belong in Next.js 16, the proxy migration, CVE-2025-29927).
  • The proxy matcher covers the paths you think it does (matcher mistakes).
  • Route handlers and actions validate input. A body you don't validate can set fields the form never shows, like user_id or role (input validation).
  • Actions return only what the UI needs. Return values go to the client, so don't return full database rows.
  • Expensive actions are rate limited: email, signups, AI calls (rate limiting Server Actions).
  • Webhooks verify their signature before acting on the payload (Stripe webhook signatures, Supabase database webhooks).
  • CORS never echoes the request origin with credentials (CORS wildcard).
  • No untrusted HTML in dangerouslySetInnerHTML, and no eval on input (XSS, eval).
  • Redirects only go to your own paths (open redirects).
  • images.remotePatterns lists real hosts, not ** (remotePatterns allow-all).

5. Dependencies and Next.js versions

You ship your dependencies as if you wrote them.

  • Next.js is at or above the security floor. As of November 3, 2026 that's 16.4.1+ on 16.x or 15.5.28 on 15.x; no 14.x is patched. Next.js also scheduled a release for November 3 fixing "seven medium vulnerabilities", so check the latest safe version before you pin. Fix: npm install next@latest.
  • npm audit is clean, or every remaining finding is understood (vulnerable npm dependencies).
  • @supabase/ssr and @supabase/supabase-js are current, so getClaims() and the new key formats work the way the docs show.

6. Account, network and ongoing checks

  • Your Supabase account has MFA, and the org enforces it on Pro, Team or Enterprise. Add a second owner so one lost login doesn't lock you out.
  • SSL enforcement and network restrictions are on under Database → Settings, if your server-side connections allow it.
  • Migrations deploy from CI, not a laptop. Supabase's production checklist recommends deploying from GitHub over manual supabase db push, and branching to test migrations.
  • The Security Advisor is clean, or every warning is a known exception (every Security Advisor warning explained).
  • You re-run this list before any release that touches auth, data access or env vars.

Which of these can be checked automatically?

About half of them. The dashboard settings and the process steps stay manual.

CheckGuardLayer static scan (code + migrations)GuardLayer Live check (your project)
Table without RLScreate table with no enable row level security, any disable row level securityTables the public key or any signed-in user can read without RLS
Over-broad grantgrant to anon or PUBLIC on a table without RLS or a view without security_invokerCovered by the RLS and view checks
View skipping RLSOnly when the same migration grants it to anonViews anon or signed-in users can read that lack security_invoker
SECURITY DEFINER functionMissing pinned search_pathSECURITY DEFINER functions anon can execute
Policiesusing (true), no user scoping, user_metadatausing (true) policies, policies that read user_metadata
Public storage bucketBucket created as publicPublic buckets
Leaked-password protectionNoYes, when you connect the project
Keys in codeservice_role or secret behind NEXT_PUBLIC_ or in client code, hardcoded JWTs and secretsNo
Next.js app layerServer Actions with no auth check, getSession() trusted on the server, matcher, CORS, webhooks, XSS, eval, open redirects, image hostsNo
Dependenciesnext and other packages below a known advisoryNo

The static scanner runs as a GitHub App on every push and PR, or on pasted code in the free Supabase security checker. It posts a check; it doesn't block a merge unless you make that check required in branch protection. The Live check reads your project's catalog (table names, RLS flags, policies, grants), never your rows, and gives a paste-ready SQL fix for each issue. Auth settings other than leaked-password protection, MFA, SSL and network restrictions aren't checked by either.

Here's a Server Action that passes code review because the UI hides it from non-admins:

guardlayer scan · app/admin/actions.tsLive engine output
Check failed
42/100 · D
  • Criticalapp/admin/actions.ts:9

    Service role key exposed to the client

    Never prefix the service role key with NEXT_PUBLIC_. Read it only in server code via process.env.SUPABASE_SERVICE_ROLE_KEY, and rotate the key immediately since it has been exposed.
  • Criticalapp/admin/actions.ts:9

    Secret exposed through NEXT_PUBLIC_

    Drop the NEXT_PUBLIC_ prefix and read the value only on the server. Publishable/anon keys are fine to expose; secret keys, tokens, and passwords are not — rotate any that have shipped.
  • Warningapp/admin/actions.ts:12

    Server Action without auth check

    At the top of every Server Action, resolve and verify the current user (e.g. const { user } = await getUser(); if (!user) throw …) and authorize the specific operation before mutating data. If you do guard it with a custom helper, this warning is a false positive.

All three findings are real. The secret key sits behind NEXT_PUBLIC_, so it's in every visitor's JavaScript and bypasses RLS from anywhere. And the action has no auth check, so anyone who can POST to your app can delete any workspace, whatever proxy.ts does. The fix is to read the key from SUPABASE_SECRET_KEY, add import 'server-only', and start the action with getClaims() plus an ownership check.

How is this different from Supabase's production checklist?

Supabase's production checklist and its linked security checklist cover the platform side well: RLS, SSL enforcement, network restrictions, account MFA, email confirmation, OTP expiry, SMTP, rate limits and backups. Every security item from them is in sections 1, 2 and 6 above. They don't cover your Next.js code: Server Actions, getSession() on the server, proxy.ts, keys in client bundles, Next.js patches, or the October 30 grant change. That's where most app-level leaks start.

FAQ

What's the most important item on a Supabase security checklist? RLS with user-scoped policies on every table in public. It's the most common real failure, CVE-2025-48757 showed it at scale, and without it the public key can read and often write every row.

Is the Supabase anon or publishable key safe to expose? Yes. It's meant to ship in the browser and is limited by your grants and RLS policies. The secret and service_role keys are the opposite: they bypass RLS and must stay on the server.

Do I still need auth checks in Server Actions if RLS is on? Yes. Server Actions and route handlers are public POST endpoints that can call other services or use the secret key, where RLS doesn't apply. Verify the user with getClaims() or getUser() and check ownership inside each one.

Do I need to change anything for the October 30, 2026 Data API change? For existing tables, no: they keep their grants. Every new table needs explicit grants in the migration that creates it, alongside enable row level security. Don't grant anon access to a table without RLS.

Does Supabase's Security Advisor cover everything on this list? No. It checks your database: RLS, views, function grants, search_path and leaked-password protection. It can't see your Next.js code, so keys in client bundles, unguarded Server Actions and outdated Next.js versions need a separate check.

How often should I run this checklist? Before launch, and before any release that touches auth, data access or environment variables. Automating the code and database checks means a new table or a renamed env var gets caught on the push that introduces it.

See which of your Supabase tables anyone can read, free

Connect your Supabase project read-only. GuardLayer checks RLS, policies, grants and functions as they are right now, and gives you the SQL to fix each problem. It never reads your rows and can't change anything.

Check my Supabase project

Prefer to check code instead? Scan your repo, no signup.

Keep reading