Developer Guide

Deploying Next.js + Supabase to Production

Ship Next.js + Supabase to production: Vercel setup, env hygiene, safe migrations, CI/CD, and the rollback paths the docs skip.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
38 min read

Deploying to production is more than pushing code—it's about reliability, performance, and maintainability. This comprehensive guide covers everything you need to deploy Next.js + Supabase applications with confidence.

Why Proper Deployment Matters#

Supabase publishes a going-into-production checklist covering the platform-side items this guide does not repeat — rate limits, backups, and the security review to run before opening a project to real traffic. Read it alongside this page rather than instead of it.

Reliability:

  • Zero-downtime deployments
  • Automatic rollbacks on failure
  • Health checks and monitoring

Performance:

  • Edge deployment for low latency
  • Optimized builds
  • CDN for static assets

Maintainability:

  • Automated CI/CD pipelines
  • Environment management
  • Database migrations

1. Deployment Platforms Overview#

Per the Next.js deployment docs, Next.js runs on any Node.js host, a Docker container, or a static export, plus first-class managed platforms. There is no single "correct" platform — pick based on where the rest of your infra already lives.

Vercel#

Pros:

  • Built by Next.js creators
  • Zero-config deployment
  • Automatic HTTPS
  • Edge network
  • Preview deployments
  • Free tier available

Best For: Next.js applications, indie developers, startups

Cloudflare Pages#

Pros:

  • Runs on the Workers edge network (often cheaper at scale for high request volume)
  • Zero-config Git integration, automatic HTTPS, preview deployments
  • Good fit if the rest of your stack (KV, R2, D1, Turnstile) is already on Cloudflare

Cons:

  • Requires the @cloudflare/next-on-pages adapter (or OpenNext) — not every Next.js feature maps 1:1 to the Workers runtime; check current compatibility before migrating
  • Edge runtime constraints apply: no synchronous file reads at request time, no raw TCP (pg/postgres need an HTTP-based driver like @supabase/supabase-js)

Best For: Teams already on Cloudflare, or optimizing for edge-network cost/latency

Netlify#

Pros:

  • Simple deployment
  • Good free tier
  • Edge functions
  • Form handling

Best For: Static sites, JAMstack applications

AWS (Advanced)#

Pros:

  • Full control
  • Scalable
  • Many services

Cons:

  • Complex setup
  • Higher cost
  • Steeper learning curve

Best For: Enterprise applications, specific requirements

Self-Hosted#

Pros:

  • Complete control
  • Cost-effective at scale

Cons:

  • Requires DevOps expertise
  • Maintenance overhead

Best For: Large applications, specific compliance needs

2. Deploying to Vercel or Cloudflare Pages#

Vercel Initial Setup#

bash
## Install Vercel CLI
npm i -g vercel
 
## Login
vercel login
 
## Deploy
vercel

Project Configuration#

json
// vercel.json
{
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm install",
  "framework": "nextjs",
  "regions": ["iad1"],
  "env": {
    "NEXT_PUBLIC_SUPABASE_URL": "@supabase-url",
    "NEXT_PUBLIC_SUPABASE_ANON_KEY": "@supabase-anon-key"
  }
}

Automatic Deployments#

Connect GitHub repository:

  1. Go to Vercel dashboard
  2. Click "New Project"
  3. Import Git repository
  4. Configure build settings
  5. Deploy

Every push to main branch triggers deployment.

Cloudflare Pages Alternative#

Cloudflare Pages builds Next.js through the @cloudflare/next-on-pages adapter (or OpenNext's Cloudflare target):

bash
## Add the adapter
npm install -D @cloudflare/next-on-pages
 
## Build for the Workers runtime, then deploy
npx @cloudflare/next-on-pages
npx wrangler pages deploy .vercel/output/static

Connect the same GitHub repository in the Cloudflare dashboard for automatic deployments on push, with preview deployments per pull request — the workflow is equivalent to Vercel's, just running on the Workers edge runtime instead of Vercel's.

Related: Deploy Next.js Supabase App to Vercel Production, Configure Custom Domain for Next.js on Vercel

3. Environment Configuration#

Environment Variables#

env
## .env.local (development)
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-local-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-local-service-key
 
## Production (set in Vercel dashboard)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-prod-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-prod-service-key

Environment-Specific Configuration#

typescript
// lib/config.ts
export const config = {
  supabase: {
    url: process.env.NEXT_PUBLIC_SUPABASE_URL!,
    anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  },
  app: {
    url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
    env: process.env.NODE_ENV,
  },
  features: {
    analytics: process.env.NODE_ENV === 'production',
    debug: process.env.NODE_ENV === 'development',
  },
}

Setting Environment Variables Per Platform#

Set variables in your platform's dashboard — the concept is the same everywhere, only the UI path differs:

  1. Vercel: Project Settings → Environment Variables → add per environment (Production, Preview, Development) → redeploy.
  2. Cloudflare Pages: Workers & Pages → your project → Settings → Environment variables → add per environment (Production, Preview) → redeploy.

Redeploy after adding or changing variables — most platforms don't hot-reload env vars into an already-running deployment.

Related: Implement Environment Variables Best Practices Next.js, Deploy Next.js 15 to Vercel Without Environment Variable Errors

4. Database Migrations#

Supabase CLI Setup#

bash
## Install Supabase CLI
npm install supabase --save-dev
 
## Initialize Supabase
npx supabase init
 
## Link to remote project
npx supabase link --project-ref your-project-ref

Create Migrations#

bash
## Create new migration
npx supabase migration new add_posts_table
sql
-- supabase/migrations/20260216000000_add_posts_table.sql
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  title TEXT NOT NULL,
  content TEXT,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
 
-- RLS policies
CREATE POLICY "Users can view own posts"
  ON posts FOR SELECT
  USING (auth.uid() = user_id);
 
CREATE POLICY "Users can create posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = user_id);

Apply Migrations#

bash
## Apply to local database
npx supabase db push
 
## Apply to production
npx supabase db push --db-url "postgresql://..."

Migration Best Practices#

sql
-- ✅ Good: Reversible migrations
CREATE TABLE posts (...);
 
-- Add rollback in separate file
-- supabase/migrations/20260216000001_rollback_posts.sql
DROP TABLE IF EXISTS posts;
 
-- ✅ Good: Add columns with defaults
ALTER TABLE posts ADD COLUMN status TEXT DEFAULT 'draft';
 
-- ❌ Bad: Breaking changes without migration path
ALTER TABLE posts DROP COLUMN important_field;

Related: Zero-Downtime Supabase Migrations: Expand/Contract (2026), Supabase Database Schema Design for SaaS Applications

5. CI/CD Pipelines#

See the Next.js deployment docs for the current supported deployment targets (managed platform, Node.js server, Docker, static export) — the CI/CD shape below (test → build → deploy) applies to any of them, only the deploy step changes.

GitHub Actions#

yaml
## .github/workflows/deploy.yml
name: Deploy to Production
 
on:
  push:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm test
        
      - name: Run linter
        run: npm run lint
        
      - name: Type check
        run: npm run type-check
 
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v20
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

On Cloudflare Pages, swap the deploy step for cloudflare/pages-action (or run npx wrangler pages deploy directly) — the test job above is unchanged, only the deploy action differs per platform.

Database Migration in CI/CD#

yaml
## .github/workflows/migrate.yml
name: Database Migration
 
on:
  push:
    branches: [main]
    paths:
      - 'supabase/migrations/**'
 
jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Supabase CLI
        uses: supabase/setup-cli@v1
        
      - name: Run migrations
        run: npx supabase db push --db-url ${{ secrets.DATABASE_URL }}

Related: Set Up CI/CD Pipeline for Next.js with GitHub Actions, Deploy Next.js Supabase App to Vercel Production

6. Monitoring and Logging#

Error Tracking with Sentry#

bash
npm install @sentry/nextjs
typescript
// sentry.client.config.ts AND sentry.server.config.ts — same init on both
import * as Sentry from "@sentry/nextjs"
 
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
  environment: process.env.NODE_ENV,
})

Application Logging#

typescript
// lib/logger.ts
type LogLevel = 'info' | 'warn' | 'error'
 
export function log(level: LogLevel, message: string, meta?: any) {
  const timestamp = new Date().toISOString()
  
  const logEntry = {
    timestamp,
    level,
    message,
    ...meta,
  }
 
  if (process.env.NODE_ENV === 'production') {
    // Send to logging service
    fetch('/api/logs', {
      method: 'POST',
      body: JSON.stringify(logEntry),
    })
  } else {
    console[level](logEntry)
  }
}

Performance Monitoring#

typescript
// lib/monitoring.ts
export function trackPerformance(metric: string, value: number) {
  if (process.env.NODE_ENV === 'production') {
    // Send to analytics
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify({ metric, value, timestamp: Date.now() }),
    })
  }
}
 
// Usage
const start = Date.now()
await fetchData()
trackPerformance('data_fetch_time', Date.now() - start)

Related: Monitor Next.js Application Performance in Production, Set Up Error Tracking with Sentry in Next.js

7. Custom Domain Configuration#

Add a Domain#

The exact DNS records differ per platform (Vercel gives you an A record + CNAME, Cloudflare Pages gives you a CNAME to <project>.pages.dev and can proxy DNS itself if your domain already lives on Cloudflare), but the flow is the same:

  1. Go to your project's domain settings (Vercel: Project Settings → Domains; Cloudflare Pages: your project → Custom domains)
  2. Add your domain (e.g., example.com)
  3. Add the DNS record the dashboard gives you — copy it exactly, it's specific to your project

SSL Certificate#

Both Vercel and Cloudflare Pages automatically provision and renew SSL certificates for custom domains — no manual certificate management needed on either platform.

Redirect Configuration#

javascript
// next.config.mjs
export default {
  async redirects() {
    return [
      {
        source: '/:path*',
        has: [
          {
            type: 'host',
            value: 'www.example.com',
          },
        ],
        destination: 'https://example.com/:path*',
        permanent: true,
      },
    ]
  },
}

Related: Configure Custom Domain for Next.js on Vercel, Deploy Next.js Supabase App to Vercel Production

8. Scaling Strategies#

Horizontal Scaling#

Both Vercel and Cloudflare Pages scale automatically based on traffic — you don't provision servers:

  • Serverless/Workers functions scale independently per request
  • Edge functions/routes deployed globally
  • Static assets served from a CDN

Database Scaling#

sql
-- Add indexes for frequently queried columns
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
 
-- Use connection pooling
-- Configure in Supabase dashboard: Database → Connection Pooling

Caching Strategy#

typescript
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }, // Revalidate every 60 seconds
  })
  return res.json()
}

Edge Functions#

typescript
// app/api/edge/route.ts
export const runtime = 'edge'
 
export async function GET(request: Request) {
  // Runs on edge, close to users
  return new Response('Hello from the edge!')
}

Related: Next.js Edge Runtime vs Node Runtime When to Use, Supabase Database Query Optimization

9. Disaster Recovery#

Database Backups#

Supabase automatically backs up your database:

  • Point-in-time recovery (PITR) available on Pro plan
  • Daily backups retained for 7 days
  • Manual backups via CLI
bash
## Create manual backup
npx supabase db dump -f backup.sql
 
## Restore from backup
psql -h db.your-project.supabase.co -U postgres -d postgres -f backup.sql

Rollback Strategy#

bash
## Rollback to previous deployment (Vercel)
vercel rollback
 
## Cloudflare Pages: roll back by redeploying a previous build from the
## dashboard's Deployments tab, or check `npx wrangler pages deployment --help`
## for the current CLI rollback command (wrangler's CLI surface changes
## between versions — verify against your installed version).
 
## Rollback database migration
npx supabase migration down

Health Checks#

typescript
// app/api/health/route.ts
import { createClient } from '@/lib/supabase/server'
 
export async function GET() {
  try {
    const supabase = createClient()
    
    // Check database connection
    const { error } = await supabase.from('posts').select('count').limit(1)
    
    if (error) throw error
    
    return Response.json({ status: 'healthy' })
  } catch (error) {
    return Response.json({ status: 'unhealthy', error: error.message }, { status: 500 })
  }
}

10. Security Best Practices#

Supabase's own going-into-production checklist and the Next.js headers() config reference cover the platform-level and framework-level pieces this section only summarizes — read both before shipping.

Environment Variables#

typescript
// ✅ Good: Server-side only
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
 
// ❌ Bad: Exposed to client
const serviceKey = process.env.NEXT_PUBLIC_SERVICE_KEY

API Rate Limiting#

typescript
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
 
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
})
 
export async function checkRateLimit(identifier: string) {
  const { success } = await ratelimit.limit(identifier)
  return success
}

CORS Configuration#

typescript
// middleware.ts
export function middleware(request: Request) {
  const response = NextResponse.next()
  
  response.headers.set('Access-Control-Allow-Origin', 'https://www.iloveblogs.blog')
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
  
  return response
}

Security Headers#

javascript
// next.config.mjs
export default {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'Referrer-Policy',
            value: 'origin-when-cross-origin',
          },
        ],
      },
    ]
  },
}

Related: Implement API Rate Limiting in Next.js Applications, Implement Environment Variables Best Practices Next.js

11. Common Deployment Issues#

Build Errors#

Problem: Module not found after deploy

Solution:

bash
## Clear cache and rebuild
rm -rf .next node_modules
npm install
npm run build

Environment Variable Issues#

Problem: Environment variables not available

Solution:

  • Ensure variables are set in Vercel dashboard
  • Prefix client-side variables with NEXT_PUBLIC_
  • Redeploy after adding variables

Database Connection Issues#

Problem: Can't connect to Supabase

Solution:

typescript
// Check connection pooling settings
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  {
    db: {
      schema: 'public',
    },
    auth: {
      persistSession: true,
    },
  }
)

Related: Fix Next.js Build Error Module Not Found After Deploy, Deploy Next.js 15 to Vercel Without Environment Variable Errors

12. Deployment Checklist#

Pre-Deployment#

  • All tests passing
  • Linter passing
  • Type checking passing
  • Environment variables configured
  • Database migrations ready
  • Security headers configured
  • Error tracking setup
  • Performance monitoring setup

Deployment#

  • Deploy to staging first
  • Run smoke tests
  • Check error logs
  • Verify database migrations
  • Test critical user flows
  • Monitor performance metrics

Post-Deployment#

  • Verify production deployment
  • Check error rates
  • Monitor performance
  • Test from different locations
  • Verify SSL certificate
  • Check custom domain
  • Monitor database performance

Frequently Asked Questions (FAQ)#

What is the best platform to deploy Next.js with Supabase?#

Vercel (built by the Next.js team) and Cloudflare Pages are both solid choices — both offer zero-config or near-zero-config deployment, automatic HTTPS, preview deployments for pull requests, and a global edge network. Vercel has the tightest first-party Next.js integration; Cloudflare Pages is a good fit if the rest of your stack is already on Cloudflare. Check the Next.js deployment docs for current platform support.

How do I handle environment variables in production?#

Set environment variables in your deployment platform's dashboard (Vercel, Netlify, etc.). Use NEXT_PUBLIC_ prefix for client-side variables. Never commit secrets to Git. Use different values for development, staging, and production environments.

Should I run database migrations before or after deployment?#

Run migrations before deployment in a separate step. Use Supabase CLI with npx supabase db push in your CI/CD pipeline. Test migrations in staging first, and always create rollback migrations for safety.

How do I implement zero-downtime deployments?#

Use platforms like Vercel that support atomic deployments. They deploy to new infrastructure, run health checks, then switch traffic. If deployment fails, traffic stays on the old version. Always test in staging first.

What monitoring should I set up for production?#

Implement error tracking (Sentry), performance monitoring (Vercel Analytics), uptime monitoring (UptimeRobot), and database monitoring (Supabase Dashboard). Set up alerts for critical errors, high response times, and downtime.

How do I handle database backups?#

Supabase automatically backs up your database daily (retained for 7 days on free tier, longer on paid plans). Pro plan includes Point-in-Time Recovery (PITR). Create manual backups before major migrations using npx supabase db dump.

Should I use connection pooling in production?#

Yes, always enable connection pooling for production. It prevents connection limit errors under load and improves performance. Enable it in Supabase Dashboard → Database → Connection Pooling and use the pooled connection string.

How do I rollback a failed deployment?#

On Vercel, use vercel rollback or roll back via the dashboard. On Cloudflare Pages, redeploy a previous build from the dashboard's deployment history. For database migrations, run your rollback migration with npx supabase migration down. Always test rollback procedures in staging.

What security headers should I configure?#

Configure X-Frame-Options (DENY), X-Content-Type-Options (nosniff), Referrer-Policy (origin-when-cross-origin), and Content-Security-Policy. Use next.config.js headers() function to set these for all routes.

How do I optimize build times in CI/CD?#

Cache node_modules and .next directories, use npm ci instead of npm install, enable Turbopack for faster builds, and run tests in parallel. Consider using GitHub Actions cache or Vercel's built-in caching.

Should I use preview deployments?#

Yes, preview deployments are essential for testing changes before production. They create a unique URL for each pull request, allowing you to test features, run E2E tests, and get stakeholder approval before merging.

How do I handle secrets in CI/CD?#

Use your CI/CD platform's secrets management (GitHub Secrets, GitLab CI/CD variables). Never commit secrets to Git. Use different secrets for staging and production. Rotate secrets regularly.

What's the best way to monitor database performance?#

Use Supabase Dashboard's Query Performance tab to identify slow queries. Monitor connection count, query execution time, and database size. Set up alerts for high CPU usage or connection limits.

How do I implement health checks?#

Create a /api/health endpoint that checks database connectivity, external service availability, and critical functionality. Use this endpoint for uptime monitoring and deployment verification.

Conclusion#

Deploying to production is a critical step in your application's lifecycle. With Vercel and Supabase, you get a powerful, scalable infrastructure without managing servers.

Focus on these key areas: automated deployments, proper environment configuration, database migrations, monitoring, and security. Start simple, then add complexity as needed.

Ship with confidence. Deploy today.

Further reading: Production RAG with Supabase pgvector and Next.js.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.