Skip to content
shipanysaas

Supabase Next.js setup: the steps, and when to buy a SaaS boilerplate

Set up Supabase with Next.js 16: packages, env vars, browser and server clients, proxy.ts session refresh. Then: build or buy the rest of your SaaS?

To set up Supabase with Next.js, create a Supabase project, install @supabase/supabase-js and @supabase/ssr, put the project URL and publishable key in .env.local, create a browser client and a server client, and refresh the session in proxy.ts (middleware.ts before Next.js 16). Server code checks the user with getClaims().

That gets you signed-in users and a database. The rest of a SaaS (teams, billing, emails, admin, tests) is the bigger job, and the second half of this post is an honest checklist for deciding whether to build it or buy a Next.js Supabase starter. A note first: this blog belongs to shipanysaas, which sells one of those starters. It comes up at the end, and the checklist works on any of them.

How do you set up Supabase in Next.js?

Six steps, all from Supabase's Next.js server-side auth guide, checked in October 2026:

  1. Create a project in the Supabase dashboard, or run supabase start for a local one.
  2. Install the packages: npm install @supabase/supabase-js @supabase/ssr (or the pnpm and yarn equivalents).
  3. Add the environment variables to .env.local: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY. The publishable key (sb_publishable_…) is safe in the browser; the secret key never goes in a NEXT_PUBLIC_ variable.
  4. Create a browser client for Client Components.
  5. Create a server client for Server Components, Server Actions and Route Handlers. It reads and writes the session cookies.
  6. Refresh the session in a proxy. Server Components can't write cookies, so proxy.ts calls supabase.auth.getClaims() on each request and passes the refreshed token on to the page and back to the browser.

Supabase suggests a lib/supabase folder with one file per client. The browser client:

import { createBrowserClient } from '@supabase/ssr';

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
  );
}

The server client gets the cookie store from next/headers:

import { cookies } from 'next/headers';
import { createServerClient } from '@supabase/ssr';

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options),
            );
          } catch {
            // Called from a Server Component: proxy.ts refreshes the session.
          }
        },
      },
    },
  );
}

The parts that catch people out:

  • The file name changed. Next.js 16 renamed middleware.ts to proxy.ts. Supabase's guide warns that on Next.js 15 and earlier a proxy.ts is never called, so sessions never refresh and users get signed out.
  • Return the right response. The proxy must return the response that carries the refreshed cookies and the Cache-Control, Expires and Pragma headers Supabase sets. Without those headers, a CDN can cache a page with one user's session and serve it to another.
  • Trust getClaims(), not getSession(). getClaims() verifies the token's signature. getSession() reads the cookie without checking it, and anyone can forge a cookie.
  • Refresh in one place. A refresh token works once (with a short grace window). Refreshing in two places at once can revoke the session, which looks like users being signed out at random.

What is Supabase Auth?

Supabase Auth is the user and session service in every Supabase project. It handles sign-up and sign-in with password, magic link, one-time codes, social login and SSO, and stores users in an auth schema in your own Postgres database, where your tables can reference them.

How does Supabase Auth work?

The client library calls the Auth service, which checks the credentials and issues a short-lived access token (a JWT) and a refresh token. Every later request to your database carries the access token, so row-level security can read the user's id with auth.uid() and return only their rows.

The client keeps the tokens in storage and swaps the refresh token for a new access token before the old one expires. In Next.js that storage is cookies, which is why the server client and the proxy exist. Per Supabase's auth architecture docs, the Auth service is Supabase's fork of GoTrue, deployed alongside your database.

When building the rest yourself makes sense

Building your own SaaS plumbing is the right call when:

  • You want to learn how it works, and you have the time.
  • Your needs are unusual: a specific identity provider, a billing model no kit handles, a data model that isn't users, teams and subscriptions.
  • You already own the parts: tested auth and billing code from an earlier product is a boilerplate already.
  • You would rewrite most of a kit anyway, because you dislike its structure.

If none fits, the question is how long you want to spend on the list below before you start on the part customers pay for.

What a Next.js SaaS boilerplate has to cover

Teams, roles and invitations

If you sell to companies, you need shared workspaces: team accounts, members with roles, permissions per role, and invitations by email. Then the edges: invitations expire, an invite link must not be forgeable, members leave, ownership moves, deleting a team should take more than one careless click, and per-seat billing has to follow the member count. Each needs a screen, a server action and a database rule. Multi-tenant SaaS architecture walks through one design.

Billing and webhooks

Checkout is the easy half. The hard half is keeping your database in step with the payment provider through webhooks. Stripe's webhook docs spell out what your endpoint must handle:

  • Verify the Stripe-Signature header with the endpoint's signing secret, against the raw request body.
  • Return a 2xx quickly, before any slow work.
  • Expect retries: in live mode Stripe keeps trying for up to three days.
  • Don't depend on order, and track event IDs to skip duplicates.

Add one rule of your own: users must never be able to write their own subscription rows. Only the verified webhook should change a plan.

Row-level security

With Supabase, browser code queries your tables directly, so every table needs RLS on, grants that match each role, policies, and tests that sign in as one customer and try to read another's data. Supabase RLS explained covers all four.

Emails, admin, tests and the rest

Invitations, one-time codes and contact-form mail from your app, plus Supabase's own auth emails, which need your own SMTP server before real users can receive them. An admin panel with a role nobody can grant themselves and two-step sign-in in front of it. End-to-end tests for sign-up, invitations and checkout, and database tests for the policies. Then a marketing site, pricing page, blog, docs, metadata, a sitemap and error monitoring.

What a kit can't do for you

  • Your product. What it does, who it's for, what each plan includes, which role may do what.
  • Rules for your own tables. A kit locks down the tables it ships. Every table you add needs its own policies and tests.
  • Your accounts and keys. Supabase, the payment provider, the email provider, your host, and the app stores if you ship a phone app.
  • Knowing what you ship. Someone has to read the policies and run the tests.
  • Certifications. No boilerplate makes your company SOC 2 or HIPAA compliant.
  • Upkeep. From the day you change it, it's your codebase, and upgrades are yours to run.

How to check a Next.js Supabase starter before you pay

  1. Sign in to the live demo. Look at a team, its members and its invitations.
  2. Find the database tests. Do they sign in as another account and try to get in? Can you run them?
  3. Read the webhook handler. Does it verify signatures? Can a signed-in user write to the subscriptions table?
  4. Read the auth setup. Does server code verify the user with getClaims(), or trust the cookie?
  5. Follow one feature through the docs. If you can't work out how invitations work, neither can your coding agent.
  6. If you build with an agent, look for an AGENTS.md, skills for the kit's own patterns, and checks the agent must run.

shipanysaas against this checklist

shipanysaas is a paid Next.js Supabase starter: a Next.js 16 web app and, in the All-in plan, an Expo app for iOS and Android, both on one Supabase backend. Its Supabase clients live in packages/supabase/src/clients, and apps/web/proxy.ts refreshes the session with getClaims().

  • Sign-in. Password, magic link, email codes, passkeys and OAuth (Google by default), each switched on with an environment variable, plus two-step sign-in with an authenticator app. See Authentication.
  • Teams. Personal and team accounts, roles, invitations that expire after 7 days, and ownership transfer and team deletion that need an emailed code. See Teams and invitations.
  • Billing. Stripe or Lemon Squeezy. The webhook checks the Stripe signature or the Lemon Squeezy HMAC, and signed-in users can only read subscriptions and orders. See Webhooks.
  • Row-level security. On for all 13 public tables. Of the 23 pgTAP test files, 13 sign in as one account and try to read or change another's data, and schema-conditions.test.sql fails if any public table has RLS off.
  • Email and admin. Transactional email through Resend, or any SMTP server with Nodemailer. A super-admin panel that needs two-step sign-in, with the admin role set on the server where users can't edit it.
  • The rest. Playwright tests, notifications, Sentry, and a blog, changelog and docs (Keystatic or WordPress). Interface text lives in message files, but only English ships.
  • For your coding agent. AGENTS.md, plus up to 49 ready-made agent skills and 17 written fixes for setup errors in All-in (Starter: 42 and 6). The product itself has no AI features.

It is a one-time purchase. Starter is the web app; All-in adds the phone app on the same backend. Prices are on the pricing page, and the live demo has a shared login and a sample team.

FAQ

Do I need middleware for Supabase in Next.js?

You need the proxy file. Server Components can't write cookies, so without it expired sessions never refresh. In Next.js 16 it is proxy.ts; in Next.js 15 and earlier it is middleware.ts, with the same Supabase code inside.

Which Supabase key goes in a Next.js app?

The publishable key goes in NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY and reaches only what your policies allow. The secret key bypasses row-level security, so it stays in server-only code. Anon key vs service role key covers the details.

Can I switch boilerplates later?

You can, but it gets expensive once you have customers. Sign-in, the account model and the billing tables are tied together, so moving means migrating users and subscriptions, not just copying code. Decide before your first paying user.

How can I tell if a starter really keeps customers' data apart?

Look for database tests that sign in as one user and try to read another account's rows, and run them. Then create two users locally and try to open one user's team as the other. A test that fails when isolation breaks is evidence; copy about security is not.