Skip to content

Next.js and Supabase, in production

Next.js on the App Router with Supabase behind it: Postgres that denies by default, access that crosses a boundary going through security-definer functions, a SQL test suite that proves the policies hold, and deploys on Vercel.

This pairing is easy to start with and easy to get wrong in exactly one place: the database is reachable from the browser. Supabase hands the client a URL and a public key, and from that moment the only thing between a curious visitor and your tables is row-level security. Most of the work on a Supabase project is therefore database work.

Deny by default

No table is readable until a policy says so. That is the state the project starts in, and it means a table added six months later is invisible rather than public. The failure mode becomes a missing feature, which somebody reports within a day, instead of a data leak, which nobody reports at all.

Access that legitimately has to cross a boundary is not granted by loosening a policy. It goes through a security-definer function with a narrow signature, so the exception is one named object that can be reviewed, rather than a widened rule that quietly applies everywhere else as well.

Proving it rather than reading it

A security model that has only been read is not a security model that has been verified. The policies are covered by a SQL test suite that runs against a throwaway Postgres instance and asserts, per role, what can and cannot be seen. A policy loosened by accident runs into that suite, so the mistake surfaces as a failing test today instead of turning up in a log six months from now.

The application side

Next.js on the App Router: server components for anything that reads data, so the query stays on the server and the session never has to travel further than it should, and client components only where there is genuine interaction. Auth runs through Supabase and the session is read on the server rather than trusted from the browser.

Types come from the database rather than being written by hand, so a renamed column shows up as a type error in the editor instead of as an undefined value at runtime. The generated types are committed and regenerated whenever the schema moves.

Running it

  • Deploys on Vercel from main, with a preview URL per branch.
  • Migrations only. An applied migration is never edited; a correction is a new one.
  • Stripe for billing and Resend for transactional email, where a project needs them.
  • Cloudflare for DNS and email routing.

A finance-content platform built this way is live in production, and it is linked below.

Common questions

Is Supabase safe to expose to the browser?
Yes if row-level security is doing its job, and not otherwise. The anon key is public by design, and it is the policies on each table that decide what a request can see. That is why the project runs deny-by-default and why the policies have a test suite around them.
What is a security-definer function for?
For the case where a query legitimately has to see more than its caller. The function runs with the privileges of its owner and has a narrow signature, so the exception is a single named object that can be reviewed, instead of a policy loosened for everybody.
Can you migrate an existing project onto Next.js and Supabase?
Usually, and the order matters: schema and policies first with tests around them, then the application, then the deployment pipeline. Moving the screens first and the access rules later is what makes these migrations run long.
Which Next.js version and rendering model?
Next.js 16 on the App Router, with server components as the default and client components only where there is real interaction. Reads happen on the server, so credentials stay there.