Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
18 minLesson 32 of 33
Testing & Deployment

Deploy to Vercel: Production Setup

Deploy to Vercel: Production Setup

Deploying a Next.js app to Vercel is fast. Deploying it correctly — with environment variables, database connections, CI/CD, custom domains, and monitoring — takes more care. This lesson covers the full production setup.

Initial Deployment

# Install Vercel CLI
npm install -g vercel

# Login
vercel login

# Deploy (from project root)
vercel

# Follow prompts:
# - Link to existing project or create new
# - Vercel auto-detects Next.js and configures build settings

Or go to vercel.com/new, import your GitHub repository, and click Deploy. Vercel detects Next.js automatically.

Environment Variables

Set these in Vercel dashboard → Project → Settings → Environment Variables:

Required for production:

DATABASE_URL          postgresql://user:pass@host:5432/db?sslmode=require
AUTH_SECRET           [32+ character random string]
AUTH_GOOGLE_ID        [from Google Cloud Console]
AUTH_GOOGLE_SECRET    [from Google Cloud Console]
NEXT_PUBLIC_APP_URL   https://yourapp.com

For your OAuth callback URLs in production, update the authorized redirect URIs:

  • Google: https://yourapp.com/api/auth/callback/google
  • GitHub: https://yourapp.com/api/auth/callback/github

Pull environment variables to your local machine:

vercel env pull .env.local

Production Database

Never use your local database in production. Use a managed provider:

Neon (recommended for Next.js):

# Install Neon serverless driver (faster cold starts)
npm install @neondatabase/serverless

# Connection string format:
DATABASE_URL=postgres://user:pass@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require

Configuration for Prisma with Neon:

// src/lib/db.ts
import { PrismaClient } from "@prisma/client";
import { Pool, neonConfig } from "@neondatabase/serverless";
import { PrismaNeon } from "@prisma/adapter-neon";
import ws from "ws";

neonConfig.webSocketConstructor = ws;

const connectionString = process.env.DATABASE_URL!;

const pool = new Pool({ connectionString });
const adapter = new PrismaNeon(pool);

const globalForPrisma = global as unknown as { prisma: PrismaClient };

export const db = globalForPrisma.prisma ?? new PrismaClient({ adapter });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;

Run migrations on deploy:

// package.json
{
    "scripts": {
        "build": "prisma generate && prisma migrate deploy && next build"
    }
}

CI/CD with GitHub Actions

Vercel deploys automatically when you push to GitHub. But you probably want to run tests first:

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL_TEST }}
      AUTH_SECRET: test-secret-for-ci
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      
      - run: npm ci
      
      - run: npm run lint
      
      - run: npm test -- --run
      
      - run: npm run build
      
      # Only deploy to Vercel on main branch push
      - name: Deploy to Vercel
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: npx vercel --prod --token ${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

Add secrets to your GitHub repository → Settings → Secrets and variables → Actions.

Custom Domain

  1. Vercel dashboard → Project → Settings → Domains
  2. Add your domain (e.g., myapp.com and www.myapp.com)
  3. Vercel shows you DNS records to add at your registrar:
    • A record: @76.76.21.21
    • CNAME: wwwcname.vercel-dns.com
  4. Wait for DNS propagation (usually under 15 minutes)
  5. SSL certificate is provisioned automatically

Preview Deployments

Every pull request gets a unique URL automatically. Use this to:

  • Share features with teammates before merging
  • Test deployment-specific issues (env vars, edge functions)
  • Let QA test without a staging server

Configure branch-specific environments in Vercel → Settings → Environment Variables → select "Preview" scope for staging values.

Performance Monitoring

Enable Vercel Analytics:

npm install @vercel/analytics @vercel/speed-insights
// src/app/layout.tsx
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html>
            <body>
                {children}
                <Analytics />
                <SpeedInsights />
            </body>
        </html>
    );
}

Vercel Analytics shows real user visits. Speed Insights shows Core Web Vitals from real users.

Edge vs Serverless Functions

// Default: Serverless function (Node.js runtime)
export async function GET() {
    const data = await db.course.findMany();   // Prisma, full Node.js
    return Response.json(data);
}

// Edge runtime: Faster cold starts, but no Node.js APIs
export const runtime = "edge";

export async function GET() {
    // Can't use Prisma here — use fetch or Neon's HTTP driver
    const data = await fetch("https://api.example.com/courses").then(r => r.json());
    return Response.json(data);
}

Use Edge runtime for:

  • Authentication middleware (already runs on edge)
  • Geolocation-based redirects
  • A/B testing at the edge

Use Node.js runtime (default) for:

  • Database queries with Prisma
  • File system access
  • Any Node.js-specific npm package

Production Checklist

✅ Environment variables set in Vercel (not hardcoded)
✅ Production database set up (Neon, Supabase, Railway)
✅ Prisma migration runs in build script
✅ Custom domain configured with HTTPS
✅ OAuth redirect URIs updated for production domain
✅ Error monitoring enabled (Sentry, Vercel logs)
✅ Analytics enabled (Vercel Analytics or Plausible)
✅ Security headers configured (HSTS, CSP, X-Frame-Options)
✅ next.config.ts has image domains configured
✅ No secrets in git history

Security Headers

// next.config.ts
const config: NextConfig = {
    async headers() {
        return [
            {
                source: "/(.*)",
                headers: [
                    { key: "X-Frame-Options", value: "DENY" },
                    { key: "X-Content-Type-Options", value: "nosniff" },
                    { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
                    { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
                ],
            },
        ];
    },
};

Next lesson: Performance optimization and Core Web Vitals — making your Next.js app fast for real users.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!