The Complete Next.js + Supabase Production Launch Checklist (47 Items)
The definitive pre-launch audit for Next.js 15 + Supabase applications. 47 concrete checks across security, RLS, performance, observability, auth, and deployment — every item caused a production incident somewhere.
The Complete Next.js + Supabase Production Launch Checklist#
Most Next.js + Supabase launches do not fail because of architecture. They fail because one item on a short, boring list never got done. A missing index. A stale environment variable. A webhook secret that was never rotated from the test value.
This checklist is 47 items that have all, at some point, caused a real production incident. Work through it before you tell paying customers they can sign up.
How to Use This Checklist#
Open it two weeks before launch. Assign each item a name and a due date. Treat yellow checkmarks with the same seriousness as red ones — a yellow means you decided not to do it, and you should write down why.
Every section below covers a single domain. You can parallelize them across a team; none of them depend on the others.
Section 1 — Security Fundamentals#
Security bugs are the ones that move you from "a quiet incident" to "an email to every affected user."
1.1 Enable Row Level Security on Every Table#
Log in to the Supabase dashboard, open the SQL editor, and run:
SELECT
schemaname,
tablename,
rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
Every row should have rowsecurity = true. If not, either enable RLS and write a policy, or document explicitly why the table is public.
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users read their own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
1.2 Audit auth.uid() Return Values in Service-Role Contexts#
auth.uid() returns NULL when called with the service role key. Any policy of the form USING (auth.uid() = user_id) will silently exclude every row during background jobs that authenticate with the service role. This is a classic silent-failure pattern.
When you need service-role reads, either wrap them in a SECURITY DEFINER function or explicitly bypass RLS with an auditable justification.
1.3 Rotate All Secrets You Copied During Setup#
List every secret set during onboarding:
- Supabase
anonkey (public, safe) - Supabase
service_rolekey (secret, rotate now) - Stripe webhook secret (rotate if it was ever pasted into a chat)
- OAuth client secrets (rotate if they appear in any README draft)
- Third-party API keys (Resend, SendGrid, OpenAI)
Rotate anything that touched a screen or a chat message. Treat your first-day secrets as compromised by default.
1.4 Lock Down CORS and Allowed Origins#
In Supabase, under Settings → API, set Allowed Origins to your production domain only. If you leave * in production, any website can call your backend from a logged-in user's browser.
1.5 Set Content-Security-Policy Headers#
Next.js middleware or next.config.mjs headers block a huge class of XSS issues.
async headers() {
return [{
source: '/(.*)',
headers: [
{ key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
],
}];
}
Loosen script-src cautiously — every third-party script is a new trust decision.
1.6 Verify Every Webhook Signature#
Stripe, Clerk, and every payment or webhook provider ships a signature verification helper. Use it.
const event = stripe.webhooks.constructEvent(
rawBody,
request.headers.get('stripe-signature')!,
process.env.STRIPE_WEBHOOK_SECRET!
);
If you catch an Invalid signature error, log it and return 400. Never fall through to process the payload.
1.7 Restrict Service Role Key to Server-Only Contexts#
Search your codebase:
grep -r "SUPABASE_SERVICE_ROLE_KEY" src/
Every match must be inside a file that runs only on the server: route handlers, server components, server actions, or API routes. Any leak into a client component or a use client boundary is a critical security bug.
Section 2 — Authentication Hardening#
Authentication is the most common regression source in the first week post-launch.
2.1 Require Email Verification Before Sensitive Actions#
Supabase ships with email confirmation off by default for development. Turn it on:
Authentication → Providers → Email → Confirm email: ON
Without confirmation, anyone can sign up with someone else's email and receive password-reset emails to their own inbox.
2.2 Set Short Session Lifetimes#
Long sessions are convenient for users and dangerous for enterprise buyers. Configure:
- Access token lifetime: 1 hour
- Refresh token rotation: enabled
- Idle timeout at middleware level
Enterprise prospects often ask for these explicitly during procurement.
2.3 Implement Middleware Session Refresh#
Without session refresh in middleware, users hit cold-cache 401s constantly. Every production Next.js + Supabase app needs this:
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
let response = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
);
},
},
}
);
await supabase.auth.getUser();
return response;
}
2.4 Handle Auth Redirect URL Allowlist#
Supabase rejects redirect targets not in the allowlist. Before launch, add every production and preview domain:
https://yourdomain.com/auth/callback
https://www.yourdomain.com/auth/callback
https://staging.yourdomain.com/auth/callback
https://*.vercel.app/auth/callback (for preview deployments)
Missing this is the #2 cause of "login works in dev, breaks in production."
2.5 Protect Every Dashboard Route#
One middleware rule covers the whole dashboard:
if (pathname.startsWith('/dashboard') && !user) {
return NextResponse.redirect(new URL('/login', request.url));
}
But also guard server components individually — middleware can be bypassed by internal rewrites in rare edge cases.
Section 3 — Database Readiness#
3.1 Index Every Foreign Key#
SELECT
c.conrelid::regclass AS table,
pg_get_constraintdef(c.oid) AS definition
FROM pg_constraint c
JOIN pg_class r ON r.oid = c.conrelid
LEFT JOIN pg_index i
ON i.indrelid = c.conrelid
AND c.conkey[1] = ANY(i.indkey)
WHERE c.contype = 'f' AND i.indexrelid IS NULL;
Any row in the result is a missing index. Add one.
3.2 Add Composite Indexes on Hot Query Patterns#
For every query of the shape WHERE tenant_id = ? AND status = ? ORDER BY created_at DESC, you want:
CREATE INDEX idx_orders_tenant_status_created
ON orders (tenant_id, status, created_at DESC);
Run EXPLAIN ANALYZE on your top 20 query patterns. Sequential scans are your enemy.
3.3 Set Statement Timeouts#
A runaway query can saturate your connection pool and bring the whole app down. Cap the worst case:
ALTER ROLE authenticated SET statement_timeout = '30s';
ALTER ROLE anon SET statement_timeout = '10s';
3.4 Enable Connection Pooling via PgBouncer#
Vercel's default setup will exhaust Supabase connections under concurrent serverless invocations. Use the Supabase pooler URL for server code:
DATABASE_URL=postgres://...@db.xxx.supabase.co:6543/postgres?pgbouncer=true
Port 6543, not 5432. Port 6543 is the pooler.
3.5 Back Up Everything Before Launch#
Schedule a daily backup. For Supabase, point-in-time recovery on Pro and above. Test a restore into a scratch project before launch day — a backup you have never restored is not a backup.
3.6 Write an Index on user_id for Every User-Scoped Table#
If the table has a user_id column, it needs an index on it. RLS policies use auth.uid() = user_id patterns, and a missing index turns every query into a full scan as your user count grows.
Section 4 — Performance Readiness#
4.1 Run Lighthouse on Every Core Route#
Set a budget: LCP under 2.5s, CLS under 0.1, INP under 200ms. Routes that do not meet it before launch will not magically improve after launch.
4.2 Eliminate Render-Blocking Scripts#
Any third-party script with strategy="afterInteractive" that is not truly required on first paint should move to strategy="lazyOnload". Analytics, chat widgets, and ad networks all belong in the lazy bucket.
4.3 Preconnect to Critical Third-Party Origins#
<link rel="preconnect" href="https://yourproject.supabase.co" />
<link rel="preconnect" href="https://images.unsplash.com" />
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
Preconnect for origins you hit on every page. DNS-prefetch for everything else.
4.4 Serve Images via next/image with Explicit sizes#
Every <Image> with fill needs a sizes prop. Without it, browsers download a 2x-wide image on every device and your mobile LCP suffers.
<Image
src={post.image}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
4.5 Use Server Components for Everything That Does Not Need Interactivity#
The default in App Router is a server component. Only add 'use client' when the file truly needs state, effects, or event handlers. Every unnecessary client component adds JS to the bundle.
4.6 Set Revalidation on Every ISR Page#
export const revalidate = 300;
export const dynamicParams = true;
Pick a value that matches your content's freshness needs — 60 seconds for news, 300 for blogs, 3600 for marketing pages. Pages without revalidate refresh on every request, which wastes server compute.
4.7 Warm the Cache with Synthetic Traffic#
Right before launch, run a script that hits every canonical route once. The first user should never be the one paying the cold-cache cost.
Section 5 — Data Integrity#
5.1 Use Database Transactions for Multi-Step Writes#
Any operation that updates more than one row across more than one table needs to be atomic.
const { data, error } = await supabase.rpc('create_order_with_items', {
order: orderPayload,
items: itemsPayload,
});
The RPC is a Postgres function that wraps both inserts in a single transaction. If any step fails, the whole operation rolls back.
5.2 Enforce NOT NULL on Every Required Column#
Null sneaks in during migrations. Audit:
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND is_nullable = 'YES'
ORDER BY table_name;
Every nullable column is a promise you made to your future self. Keep the list short.
5.3 Validate User Input at the Edge with Zod#
Validation belongs in server actions and API routes, not just on the client.
import { z } from 'zod';
const createPostSchema = z.object({
title: z.string().min(3).max(200),
content: z.string().min(10).max(50000),
tags: z.array(z.string()).max(10).optional(),
});
export async function createPost(input: unknown) {
const data = createPostSchema.parse(input);
// ... proceed with validated data
}
5.4 Add Check Constraints for Business Rules#
ALTER TABLE subscriptions
ADD CONSTRAINT status_valid
CHECK (status IN ('active', 'past_due', 'canceled', 'trialing'));
ALTER TABLE orders
ADD CONSTRAINT amount_positive
CHECK (amount_cents > 0);
The database is the last line of defense. Put the rules there.
Section 6 — Observability#
6.1 Wire Up Error Tracking Before Launch#
Sentry, Rollbar, or Bugsnag — pick one. Wire it into the Next.js error boundary and every server action.
import { captureException } from '@sentry/nextjs';
try {
await processOrder(input);
} catch (error) {
captureException(error, { extra: { input } });
throw error;
}
6.2 Log Every Request with a Correlation ID#
const requestId = crypto.randomUUID();
console.log(JSON.stringify({
requestId,
path: request.nextUrl.pathname,
userId: user?.id,
timestamp: Date.now(),
}));
When a user reports a bug with their order number, you need to trace their entire request path in 30 seconds.
6.3 Set Up Supabase Dashboard Alerts#
In the Supabase dashboard, enable alerts for:
- Database CPU over 80%
- Connection pool over 90%
- Disk space over 75%
- Auth error rate above baseline
These fire in Slack or email before users notice.
6.4 Monitor Core Web Vitals in Production#
Vercel Analytics or Google Analytics both ship field data. The lab score from Lighthouse is a starting point; the field score is what Google ranks you on.
6.5 Build a Public Status Page#
Even a simple Statuspage or Instatus page tells customers you take outages seriously. Wire incidents to it manually for the first month; automate later.
Section 7 — Deployment Hygiene#
7.1 Verify Environment Variables in Every Deployment#
Vercel will happily deploy with missing environment variables. Guard against this at build time:
// src/env.ts
import { z } from 'zod';
const envSchema = z.object({
NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
});
export const env = envSchema.parse(process.env);
Import env at the top of your app. A missing variable fails the build instead of producing a runtime 500.
7.2 Use Preview Deployments for Every PR#
Preview deploys catch migration issues, hydration errors, and build failures before merge. Every PR should link to its preview URL in the description.
7.3 Gate Deploys on CI#
At minimum: typecheck, lint, build. Ideally: tests, Lighthouse budget, bundle size check.
# .github/workflows/ci.yml
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm run build
- run: npm test
7.4 Have a Documented Rollback Plan#
Write it down: "If production breaks, the on-call engineer runs vercel rollback to the previous deployment, then supabase db restore if the database schema was touched." You do not want to figure this out during the outage.
7.5 Version Your Database Migrations#
Use the Supabase CLI:
supabase migration new add_orders_table
supabase db push
Every schema change goes through a migration file in version control. "I ran the query in the dashboard" is a launch-blocker.
Section 8 — SEO and Discovery#
8.1 Generate a Sitemap#
Split by content type if you have more than a few hundred URLs. Submit it to Google Search Console on launch day.
8.2 Set Canonical URLs on Every Page#
export const metadata = {
alternates: { canonical: 'https://yourdomain.com/page' },
};
Duplicate content is a self-inflicted wound.
8.3 Configure Robots.txt#
Exclude search results pages, filter URLs, and anything with query parameters that is not meant to be indexed.
8.4 Verify Google Search Console Before Launch#
Two options: DNS TXT record, or HTML meta tag. Either way, you should be able to see search-impression data within a week of launch.
8.5 Set Structured Data for Articles#
Every content page should emit JSON-LD: Article, BlogPosting, BreadcrumbList. Rich snippets double click-through rates on long-tail queries.
Section 9 — Legal and Compliance#
9.1 Privacy Policy and Terms of Service#
Both must be live on launch day. If you collect payments or international users, you also need a cookie consent mechanism.
9.2 GDPR Data Deletion Flow#
Users must be able to delete their account. Build the flow, test it, and document what data persists after deletion (audit logs often do, which is legal — but must be disclosed).
9.3 Data Processing Agreement Templates#
If you are selling to businesses, have a DPA template ready. Enterprise customers request these in every onboarding.
Section 10 — Operational Readiness#
10.1 Wire Up On-Call#
Even a one-person team needs an on-call process. PagerDuty, Opsgenie, or a simple phone-ring rotation. Alerts without humans attached are decoration.
10.2 Write a Runbook for Common Incidents#
For each alert you configured, write the response:
- "Supabase CPU over 80% → check current queries, identify the slow one, add an index or kill it"
- "Connection pool over 90% → check for connection leaks, restart serverless function concurrency"
10.3 Test Your Signup Flow End-to-End the Morning of Launch#
Sign up with a brand-new email. Click the verification link. Log in. Complete one payment. Cancel it. Log out. If any step breaks, the launch waits.
10.4 Have Support Contact Channels Live#
An email address, a help form, or a chat widget. Users will have problems in the first hour — they need somewhere to send them.
What to Defer Past Launch#
These feel important but are usually fine to skip for version one:
- Perfect A/B testing infrastructure (use a flag library, iterate later)
- Custom admin UI (Supabase dashboard is enough for weeks)
- Advanced analytics dashboards (Vercel + Supabase default views are enough)
- Automated load testing in CI (run it manually every quarter)
Focus on the 47 items above. The rest can catch up once you have real users telling you what hurts.
Final Pre-Launch Dry Run#
Twenty-four hours before launch:
- Run the full checklist, fix every red item, document every yellow
- Load-test at 10x expected traffic against staging
- Restore the most recent backup into a scratch project and verify data integrity
- Trigger every alert manually to confirm paging works
- Have the rollback command typed in a terminal you can paste into
Launch day is boring when you have done this right. Boring is the goal.
Related Guides#
- Building a SaaS with Next.js and Supabase for the preceding architecture work
- Multi-Tenant SaaS Architecture with Next.js and Supabase if you are launching a B2B product
- Deploying Next.js + Supabase to Production for the deployment mechanics
Conclusion#
A production launch is not a creative act. It is the execution of a checklist you prepared carefully over weeks. The creative work was the architecture, the feature design, the brand. Launch day should be the most boring day of the quarter.
Work through these 47 items. Ship with confidence. And when something still breaks in the first week — because something always does — your runbooks and observability will catch it before your users do.
Frequently Asked Questions
Related Guides
Security Best Practices for Next.js and Supabase Applications
Comprehensive security guide for Next.js and Supabase applications. Learn RLS policies, secret management, API security, authentication hardening, and production security checklist.
Complete Guide to Building SaaS with Next.js and Supabase
Master full-stack SaaS development with Next.js 15 and Supabase. From database design to deployment, learn everything you need to build production-ready...
Deploying Next.js + Supabase to Production
Complete guide to deploying Next.js and Supabase applications to production. Learn Vercel deployment, environment configuration, database migrations, CI/CD...