Next.js SaaS Deployment Checklist | Coding Capybaras

A Next.js SaaS deployment checklist for Vercel, Supabase, and Stripe: env vars, RLS, live webhooks, monitoring, and the rollback plan you need before launch.

· Justin Boggs

A person writing on a clipboard at a desk next to an open laptop

Photo by Zulfugar Karimov on Unsplash

A Next.js SaaS deployment checklist has four separate lists in it, not one — because Vercel, Supabase, Stripe, and Next.js each publish their own production checklist, and each assumes you've already handled the other three. The gaps between them are where launches break. Nobody's doc tells you that a table you created with raw SQL can reach production with row level security off, or that your Stripe test-mode price IDs don't exist in live mode, or that your NEXT_PUBLIC_ prefix decides whether a secret leaks to the browser. This is the merged list: what each vendor says, what falls between them, and the order to do it in.

TL;DR

  • Four vendors, four checklists. The failures live in the seams: env var scoping, test-vs-live Stripe objects, RLS defaults, and webhook signing secrets that differ per environment.
  • Do it in this order: env vars → database (RLS + backups) → Stripe live mode → monitoring → rollback plan. Each step depends on the one before it.
  • The two that bite hardest: a Supabase table with RLS off exposes all its data to any client holding your public anon key, and Stripe objects created in a sandbox don't exist in live mode.
  • Know how to roll back before you need to. Vercel's instant rollback works in seconds; a bad database migration doesn't.

Why four checklists instead of one?

Each vendor publishes a genuinely good production checklist. Next.js has one covering rendering, caching, and security. Vercel's is written by their engineering team and covers operational excellence, security, reliability, performance, and cost. Supabase's covers security, performance, and availability. Stripe's go-live checklist covers API versions, error handling, and live webhooks.

Every one of them is scoped to its own product. That's correct — and it's exactly the problem. Your Stripe webhook endpoint is a Vercel function that writes to a Supabase table using a service-role key stored in a Vercel environment variable. Which checklist owns that? All four, partially. None of them, actually.

So here's the merged path, in dependency order. Nothing later works if something earlier is wrong:

flowchart TD
  A[1. Environment variables<br/>scoped and secret] --> B[2. Database<br/>RLS + migrations + backups]
  B --> C[3. Stripe live mode<br/>objects + keys + webhooks]
  C --> D[4. Monitoring<br/>errors + logs + alerts]
  D --> E[5. Rollback plan<br/>code and data]
  E --> F[Launch]

The ordering isn't arbitrary. Your database migration can't run without the right connection string. Your Stripe webhook can't write a subscription record if RLS blocks it. Your monitoring can't tell you what broke if it's not wired before traffic arrives. And a rollback plan written after the incident starts isn't a plan.

I'll be honest about the scope here: this checklist assumes a solo founder or tiny team on the standard Next.js + Supabase + Stripe stack. If you have a platform team, most of Vercel's enterprise items — SCIM, audit logs, function failover, load testing — apply to you and don't to me. I'm skipping them, not dismissing them.

Step 1: Get your environment variables right

Every launch failure I've had traces back to an environment variable that was missing, wrong, or in the wrong place. Start here.

The NEXT_PUBLIC_ rule is a one-way door. Next.js inlines any variable prefixed NEXT_PUBLIC_ into the client bundle at build time. It ships to every visitor's browser, permanently, in a file anyone can read. Next.js's production checklist puts it plainly: ensure your .env.* files are in .gitignore and only public variables carry the prefix.

The practical test: could you print this value on your homepage? Your Supabase anon key, yes — it's designed for that, and RLS is what protects you. Your Stripe publishable key, yes. Your Supabase service-role key, absolutely not — it bypasses RLS entirely. Your Stripe secret key, your webhook signing secret, your Resend API key: no.

| Variable | Prefix | Lives where | If it leaks | | --- | --- | --- | --- | | Supabase URL | NEXT_PUBLIC_ | Browser + server | Fine — it's public by design | | Supabase anon key | NEXT_PUBLIC_ | Browser + server | Fine if RLS is on. Catastrophic if not | | Supabase service-role key | none | Server only | Full database access, bypasses RLS | | Stripe publishable key | NEXT_PUBLIC_ | Browser + server | Fine — designed to be public | | Stripe secret key | none | Server only | Full account access. Rotate immediately | | Stripe webhook secret | none | Server only | Attackers can forge webhook events |

That anon-key row is the whole game. The anon key is safe to publish only because RLS stands between it and your data. Which brings us to step 2.

Scope your variables per environment. Vercel gives you Production, Preview, and Development scopes. Use them. Your preview deployments should point at Stripe test mode and a non-production database — otherwise a preview branch can charge real cards. This is the single most valuable five minutes on this list.

Commit your lockfile. Vercel's checklist calls this out under security: committing pnpm-lock.yaml pins your dependencies and speeds up builds through caching. An unpinned dependency tree means your production build can differ from what you tested. Don't hand a supply-chain surface to a stranger to save a diff.

Don't source .env.local into your shell. Empty placeholder values get exported and silently shadow the real file for every process after. Use npx dotenv -e .env.local -- <command> for one-offs instead. I lost an afternoon to this one.

Step 2: Lock down the database

A table without row level security is readable and writable by anyone. This is the highest-consequence line on the entire checklist. Supabase's production checklist states it flatly: tables without RLS enabled and reasonable policies "allow any client to access and modify their data."

Worth being precise here, because the internet gets this wrong in both directions. Create a table through the Supabase dashboard's table editor and RLS is enabled for you. Create one with raw SQL — a migration file, a CREATE TABLE in the SQL editor, or your AI assistant writing schema — and it lands with RLS off unless you say otherwise. Guess which path a founder shipping with Claude Code takes. The dashboard default protects you exactly when you're least likely to need protecting.

Read that with the anon key in mind. Your anon key is in the browser bundle. Anyone can extract it in ten seconds. If RLS is off on a table, that key reads and writes it — every row, every user, all of it. RLS isn't a hardening step you get to later. It's the only thing standing between your public key and your customer data.

Supabase ships a Security Advisor in the dashboard that flags exactly this. Run it. Fix everything it finds. Then run the Performance Advisor and at minimum read what it says about indexes.

The rest of the database list, in the order I do it:

  • Enable SSL enforcement (Database → Settings → SSL Configuration). One toggle.
  • Turn on email confirmations in Authentication → Providers. Off by default in dev, and you don't want unverified emails in production.
  • Set the OTP expiry to something sane. Supabase recommends 3600 seconds (1 hour) or lower.
  • Wire up custom SMTP. This one is sneaky. Supabase's default auth email rate limit is 2 emails per hour — that's a hard stop on signups until you bring your own SMTP credentials. Even with custom SMTP, the default is 30 new users per hour, which their docs warn is likely not enough for a major public announcement. If you're launching on Product Hunt, raise this first. Ask me how I know.
  • Understand your backups. Free plan projects can't download backups at all. If you expect your database to exceed 4 GB, Supabase recommends enabling the Point-in-Time Recovery add-on, which lets you restore to any point with second-level granularity.
  • Enable MFA on your Supabase account — and on the GitHub account you sign in with, since that GitHub account holds administrative rights over your whole Supabase org.
  • Add a second org owner. If you lose access to your account, you lose access to your business. Supabase's checklist suggests considering it; I'd put it higher than that, and almost nobody does it.

One more that's easy to skip and pays for itself: connect your GitHub repo and enable Deploy to production in the Supabase integration settings. It runs schema changes consistently and gets you out of the habit of supabase db push from a laptop at 11pm. Most database schema mistakes that haunt founders start as a manual migration nobody reviewed.

Step 3: Take Stripe live

Stripe's live and sandbox environments are designed to behave near-identically, which is why the switch is mostly swapping API keys — and why the failures are the small handful of places where they don't behave identically.

Test objects don't exist in live mode. Stripe's go-live checklist spells this out: objects created in a sandbox — it names plans, coupons, products, and SKUs, and the same applies to prices — aren't usable in live mode. You have to recreate them, and critically, recreate them with the same ID values, not the same names, so your code keeps working. If your app has price_1ABC... hardcoded or in an env var, that ID is test-mode-only. This is the number one launch-day 500 error, and it fires exactly when someone tries to pay you.

Register live webhook endpoints separately. Your Stripe account has test and live webhook endpoints as distinct objects. Define the live one, point it at your production URL, and copy the new signing secret — it's different from your test secret. Then confirm the live endpoint behaves identically to the test one.

Stripe's checklist adds three requirements for your production endpoint that are worth quoting because they're where correctness actually lives. It must:

  • Handle delayed webhook notifications
  • Handle duplicate webhook notifications
  • Not require event notifications to occur in a specific order

That third one surprises people. Stripe does not guarantee ordering. customer.subscription.updated can land before checkout.session.completed. If your handler assumes a sequence, it works in dev — where you fire events one at a time with the CLI — and corrupts state in production. Write handlers that are idempotent and order-independent, or accept that you'll be reconciling by hand. I went deep on the specific failure modes in Stripe webhook hell — signature verification and idempotency are the two that cost me the most.

Rotate your keys before you launch. Stripe recommends this explicitly: rotate in case a key was saved somewhere outside your codebase during development — a Slack message, a screenshot, a scratch file. Assume every key you've had since day one is compromised, because you can't prove it isn't.

Pin your API version. Your account has an API version setting, and webhook events are structured according to it unless you override at endpoint creation. For TypeScript, the library version pins it. Upgrade deliberately, not by accident on a pnpm update.

Finally: test with bad data. Stripe's checklist recommends testing with incomplete data, invalid data, and duplicate data — retry the same request and see what happens — and having someone who isn't you try to break it.

Step 4 and 5: Monitoring and the rollback plan

You cannot fix what you can't see. Wire observability before launch, not after your first incident.

Errors. Sentry into a Next.js SaaS is a ten-minute job and the free tier is generous. Without it, your first signal that checkout is broken is a customer email — if they bother, which mostly they don't. They just leave.

Logs. Vercel's checklist recommends enabling Log Drains to persist logs from your deployments. Vercel's runtime logs are ephemeral; when you're debugging something that happened four hours ago, "ephemeral" means "gone." Stripe's checklist makes the parallel point for payments: log important data on your end despite the apparent redundancy, because your logs are the backup if your server can't reach Stripe at all.

Cost alerts. Configure Spend Management on Vercel and set an alert threshold. A runaway function or an unexpected traffic spike shouldn't be something you discover on an invoice.

Security headers. Vercel's checklist recommends a Content Security Policy and proper security headers, plus Deployment Protection so your preview deployments aren't publicly indexable. That last one matters more than it sounds — preview URLs get crawled, and yours might be pointing at a real database.

Then the part everyone skips until it's too late.

Know how to roll back before you need to. Vercel's operational excellence section leads with defining an incident response plan — escalation paths, communication channels, rollback strategies — and getting familiar with how to stage, promote, and roll back deployments before launch. Vercel's instant rollback promotes a previous deployment in seconds. Practice it once on a Tuesday afternoon when nothing is wrong. The middle of an incident is a bad time to read documentation.

The asymmetry worth internalizing: code rolls back in seconds; data doesn't roll back at all. A bad deploy is a click. A migration that dropped a column is a restore-from-backup, and on the Supabase free plan you can't download that backup. This is why the database step comes before the deploy step, and why "what's my rollback for this migration?" is a question to answer while writing it, not while your users are staring at a 500.

Add a status page when you have enough customers that "is it down or is it me?" becomes a support burden. Not before — it's one more thing to maintain.

Frequently asked questions

What's the single most common Next.js SaaS deployment mistake?

Shipping Supabase tables with row level security off. Your anon key is in the browser bundle by design, and RLS is the only thing preventing that key from reading every row in an unprotected table. Run Supabase's Security Advisor before launch and fix everything it flags.

Do I need all of Vercel's production checklist items?

No. Vercel's checklist covers Enterprise features — SCIM, audit logs, function failover, load testing — that don't apply to a solo founder (SAML sits in between: it's a Pro add-on or Enterprise). The universally relevant ones are security headers, Deployment Protection, Log Drains, Spend Management, and knowing how to roll back.

Why do my Stripe price IDs break in production?

Because objects created in a sandbox don't exist in live mode. Stripe's go-live checklist is explicit about this. Recreate your products, prices, and coupons in live mode using the same ID values — not the same names — so your existing code keeps working.

Does Supabase enable RLS on new tables automatically?

Only for tables created through the dashboard's table editor. Tables created with raw SQL — a migration file, the SQL editor, or schema your AI assistant wrote — land with RLS off unless you enable it explicitly. That's the common path for anyone shipping with Claude Code, so verify with the Security Advisor rather than assuming.

How do I stop preview deployments from touching production data?

Scope your environment variables per environment in Vercel. Production, Preview, and Development are separate scopes: point Preview at Stripe test mode and a separate database. Otherwise a preview branch can charge real cards.

Should I run next build locally before deploying?

Yes. Next.js's production checklist recommends running next build to catch build errors and next start to measure performance in a production-like environment. One caveat: don't run a build while your dev server is running — both write to .next/ and corrupt each other's output, producing fake regressions you'll waste an hour chasing.

What breaks first when I get a traffic spike?

Auth emails, usually. Supabase's default rate limit is 2 auth emails per hour without custom SMTP, and 30 new users per hour with it. If you're planning a Product Hunt launch, raise those limits days ahead — a signup flow that silently stops sending confirmation emails looks exactly like a working site.

The short version

A Next.js SaaS deployment checklist is really four vendor checklists stitched together, and the failures live in the seams none of them own. Work in dependency order: scope your environment variables and get NEXT_PUBLIC_ right, turn on RLS and verify it with Supabase's Security Advisor, recreate your Stripe objects in live mode with matching IDs and register a live webhook endpoint with its own signing secret, wire error tracking and log drains before traffic arrives, and rehearse your rollback while nothing is broken. Do those five and launch day is boring. Boring is the goal.

If you're shipping a SaaS with AI coding tools, Coding Capybaras is the free boilerplate I built for exactly this workflow — the deployment wiring above comes pre-configured, so the checklist is mostly verification instead of construction.