Guides

Next.js and Supabase for a SaaS MVP: Architecture, Auth, Billing and the Parts People Underestimate

How to structure a Next.js and Supabase SaaS: where auth checks belong, Row Level Security for teams, Stripe webhooks as the billing record, and the five setup jobs that bite after launch.

Summarize with AI
Octo Agency cover: Next.js and Supabase for a SaaS MVP, with a purple hexagon pattern

Supabase's own documentation puts the biggest risk in one line: "A table in an exposed schema without RLS is readable and writable by any role with a grant on it." In a Next.js and Supabase SaaS, the browser talks to your database through Supabase's API, so Row Level Security is not a hardening step for later. It is the permission system.

This guide covers how we would structure a SaaS MVP on Next.js and Supabase, with Stripe for billing: what runs where, where the auth checks belong, how to model teams, why the webhook is the billing record, and the setup jobs that tend to surface after launch. Every limit and default below comes from the Supabase, Next.js and Stripe documentation, read on 28 October 2026. Our bias: we build applications in Next.js.

What runs where in a Next.js and Supabase SaaS

The stack is small, but each piece has a narrow job. The common security mistakes come from code doing a job that belongs to a different layer.

Responsibilities by layer in a Next.js, Supabase and Stripe SaaS
LayerIts jobNot its job
Proxy (proxy.ts, middleware.ts before Next.js 16)Refresh the Supabase session cookie, redirect signed-out usersThe only authorisation check
Server Actions and Route HandlersMutations, each with its own auth checkAssuming the button was hidden so nobody can call it
Postgres with Row Level SecurityDecide which rows each user and team can see and changeBeing optional because the UI filters data
Stripe and the webhook routeCharge cards, own subscription state, notify your appBeing queried live on every page load
A subscriptions table in your databaseThe app's copy of who has paid for whatBeing written to by anything except the webhook

Source: Supabase server-side auth and RLS docs, Next.js authentication guide (updated 2026-09-16), Stripe subscription webhooks docs

Postgres is the boundary: it should refuse a query for another customer's rows even when a Server Action forgets a check. Stripe is the billing record, copied into your database by webhooks so no page load waits on Stripe's API. Our post on The MVP Tech Stack We Recommend in 2026 covers vendor choice and cost; this page is about wiring them together.

Auth: cookies, the server check and the proxy trap

Supabase's Next.js setup uses `@supabase/ssr` and two clients: a browser singleton, and a server client created per request because it needs that request's cookies.

The proxy refreshes the session. The file is `proxy.ts` in Next.js 16 and `middleware.ts` in 15 and earlier; get it wrong and it never runs, so sessions never refresh. Refresh in one place only, because refresh tokens are generally single-use and two refreshers can sign people out.

The server check uses `getClaims()`. Supabase says never to trust `getSession()` in server code, because it reads the cookie without revalidating it. `getClaims()` verifies the token's signature on every call.

The proxy is not the gate. The Next.js authentication guide calls proxy checks "optimistic", puts real checks in a Data Access Layer close to the data, and says to treat Server Actions and Route Handlers like public API endpoints. CVE-2025-29927, published on 21 March 2025 with a CVSS score of 9.1, let attackers bypass authorisation in middleware on self-hosted Next.js before 15.2.3, 14.2.25, 13.5.9 and 12.3.5. Apps whose only check sat there were open.

Email will stop you before code does. Supabase's built-in sender is "not meant for production use": it delivers only to your project's team, 2 messages an hour. Custom SMTP starts at 30 an hour, adjustable. Confirmations, magic links and resets share that budget, so raise it before launch.

Teams and Row Level Security

B2B SaaS sells to companies, so the data model needs an organisation from day one. Retrofitting it means rewriting every policy.

The shape we would start with. An `organisations` table, a `memberships` table linking users to organisations with a role, and `organisation_id` on every customer table. Each policy asks one question: is this user a member of the organisation that owns the row?

Write policies the fast way. Supabase lists three high-impact rules: index every column a policy filters on; write `(select auth.uid())` so Postgres evaluates it once per statement, not per row; and name the role with `to authenticated`. Its performance guide also advises against joins in policies: fetch the user's organisation IDs into a set and test with `in` or `any`.

Know what bypasses it. A secret key uses the `service_role` role, which has `bypassrls`. Webhooks and admin jobs use it; the browser never sees it. If a user's access token is attached, though, the request runs under that user's policies.

Revoke grants, do not just add policies. Adding policies does not remove default grants to `anon` and `authenticated`. With RLS on and no policies, nothing is reachable with a publishable key, a safe default for unfinished tables.

Billing: the webhook is the record

Stripe says subscriptions need a webhook destination "because most subscription activity happens asynchronously".

Stripe events and statuses a SaaS MVP has to handle
Event or statusWhen it happensWhat the app should do
`customer.subscription.created`Subscription starts; status may be `incomplete`Store it, grant nothing yet if incomplete
`invoice.paid`An invoice is paidRetrieve the subscription, confirm `active`, extend access
`invoice.payment_failed`A renewal charge failsEmail the customer, show a banner, keep access while retries run
`customer.subscription.trial_will_end`3 days before a trial endsCheck a payment method exists, warn the customer
`customer.subscription.deleted`Subscription endsRevoke paid features
`incomplete`First payment not completedCustomer has 23 hours to pay, then `incomplete_expired`
`past_due`Latest invoice failed or was not attemptedAsk for new card details
`canceled` or `unpaid`Retries exhausted, per your settingsRevoke access

Source: Stripe, Using webhooks with subscriptions, read 2026-10-28

Make the handler idempotent. Stripe can send an event twice and does not guarantee order. Log event IDs with a unique constraint and skip repeats. Do not order by `created`, since events can share a second; fetch the current subscription and write that.

Verify, then answer fast. Signature checks need the raw body, so read it as text first, and return a 2xx before slow work. Stripe retries for up to three days in live mode, and failing `invoice.created` can delay finalisation by up to 72 hours.

Let Stripe host what it can. The customer portal covers card updates, plan changes, cancellation and invoices, though not inside an iframe. Smart Retries' recommended default is 8 tries within 2 weeks.

The parts people underestimate

Each is invisible in a demo and expensive the first time it fails.

Five setup jobs that surface after launch
PartWhat the docs sayWhat to do in week one
Database connections from serverless functionsTransaction mode on port 6543 is for "serverless and edge functions"; prepared statements are not supported thereUse the pooler URL, one connection per client, `prepare: false`
Schema changesEditing production in the SQL or Table Editor bypasses migration history and breaks `db push`Keep every change in `supabase/migrations`, deploy with `supabase db push` from CI
BackupsPro keeps 7 days of daily backups, Free none; Storage objects are not includedPro from day one; back up Storage separately; PITR is about $100 a month for 7 days
Free-tier pausingFree projects pause after 1 week of inactivityNo paying customer on a Free project
Auth rate limitsSign-ups and sign-ins: 30 per 5 minutes per IP; token endpoint: 150 per 5 minutes per IPRaise limits before a launch or a single-office customer onboards together

Source: Supabase docs on connecting to Postgres, database migrations, backups, auth rate limits and pricing, read 2026-10-28

The rate limits are counted per IP address, so 40 staff accepting invitations from one office network can hit 30 requests in 5 minutes. Point-in-time recovery costs $0.137 an hour and needs at least the Small compute add-on.

The build order we would follow

This is the order we would use, not a record of past projects.

Week one. Supabase Pro, migrations in git, organisations and memberships with RLS on every table, `getClaims()` in a Data Access Layer, custom SMTP and error tracking.

Next, the core loop. The workflow a customer pays for, every Server Action calling the Data Access Layer.

Then billing. Checkout, the portal, a verified webhook with an events table, and a subscriptions table only the webhook writes, tested with the Stripe CLI.

Before launch. Sign in as organisation A and try to read organisation B's rows through the API. If anything returns, a policy is missing.

Our MVP Spec Template post has the one-page brief we would ask for before quoting this work, and Supabase vs Firebase for a B2B SaaS MVP explains why we lean to Postgres for team-based data.

What this means for founders planning a Next.js and Supabase SaaS

The framework and database are the easy decisions. The time goes into the joins: the auth check in three places, the team policies, and a webhook that stays correct when events arrive late, twice or out of order. Budget for those as features.

If you are comparing quotes, ask each team how they handle the five setup jobs in the table above. A team that names RLS performance rules, webhook idempotency and Storage backups without prompting has built this before. Our web app development service page explains how we scope a Next.js build like this one.

Frequently asked questions

Is Supabase secure enough for a B2B SaaS?

It is as secure as its Row Level Security policies. Supabase's documentation warns that a table in an exposed schema without RLS is readable and writable by any role with a grant on it, and that adding policies does not remove existing grants. Enable RLS on every table, write policies that check organisation membership, keep the secret key on the server, and test with two accounts from different organisations before launch.

Should I put auth checks in Next.js middleware?

Use proxy, called middleware before Next.js 16, only to refresh the session and redirect signed-out users. The Next.js authentication guide calls these checks optimistic and says most security checks belong close to the data source, in a Data Access Layer. CVE-2025-29927, rated 9.1 and published in March 2025, let attackers bypass authorisation that lived only in middleware on unpatched self-hosted versions.

What is the difference between getSession, getUser and getClaims in Supabase?

`getSession()` reads the session from the cookie without revalidating it, so Supabase says never to trust it in server code. `getClaims()` verifies the access token's signature on every call and suits most server checks. `getUser()` asks the Supabase Auth server for a fresh user record, which costs a network call but reflects changes such as a deleted or banned user. Use `getClaims()` in proxy and the Data Access Layer.

How should a Next.js app handle Stripe webhooks?

Create a Route Handler that reads the raw body, verifies the Stripe-Signature header and returns a 2xx quickly. Stripe retries failed deliveries for up to three days in live mode, does not guarantee event order and can deliver an event twice, so store processed event IDs and skip repeats. On `invoice.paid`, retrieve the subscription and confirm it is `active` before extending access. Write subscription state to your own table.

How much does Supabase cost for a SaaS MVP?

Supabase Pro starts at $25 a month and includes 100,000 monthly active users, 8 GB of database disk, 100 GB of file storage, 7 days of daily backups and a $10 compute credit, with a spend cap on by default. The Free plan pauses projects after a week of inactivity and has no daily backups, so it suits prototypes rather than paying customers. Point-in-time recovery adds about $100 a month for 7 days.

Sources

  1. Row Level Security Supabase Docs
  2. RLS Performance and Best Practices Supabase Docs
  3. Setting up Server-Side Auth for Next.js Supabase Docs
  4. Send emails with custom SMTP Supabase Docs
  5. Rate limits Supabase Docs
  6. Connect to your database Supabase Docs
  7. Database migrations Supabase Docs
  8. Database backups Supabase Docs
  9. Pricing and Fees Supabase
  10. How to implement authentication in Next.js Next.js documentation, Vercel, 2026
  11. Authorization bypass in Next.js middleware, GHSA-f82v-jwr5-mffw Vercel via GitHub Security Advisories, 2025
  12. Receive Stripe events in your webhook endpoint Stripe Docs
  13. Using webhooks with subscriptions Stripe Docs
  14. Automate payment retries Stripe Docs
  15. Provide a customer portal to your customers Stripe Docs

Tags

  • Next.js
  • Supabase
  • SaaS
  • MVP
Mihajlo Stojanovic

Mihajlo Stojanovic

Co-founder, Octo Agency

Mihajlo runs the technical side of Octo Agency: Webflow builds, custom backends and the systems that keep a site editable after launch. He has shipped more than twenty Webflow sites a year as a white-label partner and writes about what survives contact with a real marketing team.

Veljko Nikolic

Reviewed by

Veljko Nikolic

Co-founder, Octo Agency

Veljko runs the build side of Octo Agency. He has shipped marketing sites for B2B teams in legal tech, health and manufacturing, and writes about what survives contact with a real review cycle.