BoilerplateHub

Next.js + Supabase + Better Auth + Stripe

Next.js with Supabase, Better Auth and Stripe keeps the Postgres but moves the user tables under your own migrations. Better Auth writes sessions into the same database your application queries, and Stripe leaves billing logic in your route handlers. It suits a web SaaS whose team wants to read the source of its own auth layer.

Next.js SupabaseBetter AuthStripe

Database

Supabase

Supabase here is deliberately reduced to its Postgres half. Better Auth creates user, session and account tables through migrations you run, so the managed auth schema goes unused and row level security has no Supabase JWT to read. Query through the connection string from route handlers rather than the browser client, and keep Stripe subscription rows in the same schema so an entitlement check is a join rather than a second network call.

Authentication

Better Auth

Better Auth suits this because the Stripe customer ID belongs on a row next to the session's user, joined by a real foreign key instead of mirrored across systems. Plugins add organizations and two factor without introducing a second vendor, and organizations matter the moment seats are billed. Next.js middleware reads the session cookie and server components resolve it before render, so a paywalled page is correct on first paint. You own the patching schedule for that code.

Payments

Stripe

Stripe gives you the full billing vocabulary: trials, proration, seats, usage records and invoice customization, all driven from your own route handlers. Because Stripe is not merchant of record, the subscription state written into Postgres is the authoritative copy your application reads, and you are the party registered for VAT and sales tax wherever you sell. That obligation buys pricing experiments that ship as a deploy rather than a support request.

What to watch out for

The tempting mistake is reaching for Supabase row level security to protect data that Better Auth is now guarding. Policies expect a Supabase-issued token that Better Auth never mints, so a condition written against auth.uid() quietly evaluates to null and the policy either blocks everyone or nobody. Decide that authorization happens in server code, connect with a privileged role, and keep the anon key out of the browser entirely in this configuration.

Boilerplates close to this stack

Matched on Next.js plus the parts of this stack our catalog tags. Each card shows which pieces actually line up, so you can see how much you would still wire yourself.

A CLAUDE.md for this stack

The rules that matter for this combination specifically, including who owns entitlement state. Adapt the commands to your repository before committing it.

CLAUDE.md
# CLAUDE.md

This project is a Next.js SaaS on Supabase, Better Auth and Stripe.

## Stack

- Framework: Next.js
- Database: Supabase
- Auth: Better Auth
- Payments: Stripe

## Boundaries

- Never edit a migration that has already run. Write a new one.
- Never hardcode Stripe price or product identifiers in components. They belong in config.
- Never trust a client-supplied user id. Read the session from Better Auth on the server.
- Treat webhook handlers as idempotent. The same event will arrive twice.

## Entitlements

- Stripe is the source of truth for what a customer paid for.
- The database mirrors that state; it never decides it.
- Any check for "can this user do X" reads the mirrored entitlement, not a live API call.

## Before you say a change is done

- The app builds.
- Tests pass.
- No secret, key or webhook signing secret appears in a committed file.

Related stacks