DEV Community

Cover image for Next.js + Supabase Production Launch Checklist (47 Items)
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Next.js + Supabase Production Launch Checklist (47 Items)

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';
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 anon key (public, safe)
  • Supabase service_role key (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' },
    ],
  }];
}
Enter fullscreen mode Exit fullscreen mode

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!
);
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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(),
}));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' },
};
Enter fullscreen mode Exit fullscreen mode

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:

  1. Run the full checklist, fix every red item, document every yellow
  2. Load-test at 10x expected traffic against staging
  3. Restore the most recent backup into a scratch project and verify data integrity
  4. Trigger every alert manually to confirm paging works
  5. 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

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.


Originally published at https://www.iloveblogs.blog

Top comments (0)