Skip to content
Lesson 20 of 22

From Code to Production

14 min read

The Production Mindset

Your app works perfectly on localhost. Congratulations — you've cleared the first hurdle. But here's the reality check: localhost is a fantasy land. There are no bad actors trying to inject SQL into your forms. There are no users on flaky 3G connections in rural areas. There's no one hammering your API with 10,000 requests per second. There's just you, your browser, and a comforting sense of control.

Production is different. Production means real users with real data generating real traffic, and every one of those "reals" introduces problems you've never thought about. A form that works beautifully when you fill it out yourself breaks spectacularly when someone pastes 50,000 characters of emoji into the name field. A page that loads instantly on your M3 MacBook crawls on a five-year-old Android phone. An API endpoint that handles your test data gracefully chokes when the database has a million rows.

The production mindset isn't about fear — it's about respect. Respect for your users, respect for their data, and respect for the fact that things will go wrong. Your job is to make sure that when they do, you know about it before your users start tweeting about it.

The good news? As a vibe coder, you don't need to memorize every production configuration. You need to know what matters and how to prompt AI to implement it correctly. That's what this lesson is about.

Production Readiness Checklist

Before you deploy anything, run through this checklist. Print it out, tape it to your wall, or better yet, turn it into a GitHub issue template. Every item here is something that has bitten a developer in production:

Error Handling:

  • All API routes return proper error responses (not stack traces)
  • Client-side error boundaries catch rendering failures
  • Form validation shows user-friendly messages
  • Network failures are handled gracefully with retry logic

Security:

  • All environment variables are properly configured (never hardcoded)
  • HTTPS is enforced everywhere
  • Security headers are configured (we'll cover these below)
  • Input validation exists on both client and server
  • SQL injection and XSS vectors are mitigated
  • Authentication tokens have proper expiration

Performance:

  • Images are optimized (proper formats, responsive sizes)
  • Fonts are optimized (self-hosted, display swap)
  • Loading states exist for all async operations
  • Bundle size is reasonable (no importing all of lodash for one function)

User Experience:

  • Custom 404 page exists
  • Custom 500 error page exists
  • Favicon is set
  • Meta tags are configured for every page
  • Open Graph images work for social sharing
  • The app is responsive on mobile

Operations:

  • Error monitoring is configured (Sentry or similar)
  • Analytics are tracking key events
  • Environment variables are set in your hosting platform
  • Database backups are automated

This looks like a lot. It is a lot. But here's the vibe coding approach: paste this checklist into your AI tool and say "audit my project against this checklist and fix everything that's missing." You'll be surprised how much it can handle in one pass.

CI/CD with GitHub Actions

What CI/CD Is

CI/CD stands for Continuous Integration and Continuous Deployment. In plain terms: every time you push code, a robot checks your work and (if everything passes) deploys it automatically.

Without CI/CD, your deployment process looks like this: finish a feature, remember to run tests, hope you didn't forget anything, manually deploy, pray nothing breaks. With CI/CD, you push code and the pipeline handles everything: linting, type checking, testing, building, and deploying. If anything fails, you get a notification before broken code ever reaches your users.

Creating Your CI Workflow

GitHub Actions uses YAML files in a .github/workflows/ directory. Here's a solid starting workflow:

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

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

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Type check
        run: npm run typecheck

      - name: Run tests
        run: npm run test

      - name: Build
        run: npm run build

This workflow triggers on every push to main and on every pull request targeting main. It runs your linter, TypeScript compiler, tests, and build — in that order. If any step fails, the pipeline stops and you see exactly what broke.

The Prompt That Writes Your CI

Here's the prompt you can use:

Create a GitHub Actions CI workflow for my Next.js project. It should:
- Run on pushes to main and on pull requests
- Install dependencies with npm ci
- Run lint, typecheck, tests, and build in sequence
- Cache node_modules for faster runs
- Use Node.js 20
Save it to .github/workflows/ci.yml

The AI will generate the file and often add improvements you didn't think of, like caching strategies or parallel jobs for larger projects.

Deploying to Vercel

Vercel is the company behind Next.js, and their platform is built specifically for deploying Next.js applications. The deployment experience is as close to magic as web hosting gets.

Connecting Your Repository

  1. Go to vercel.com and sign in with GitHub
  2. Click "Add New Project"
  3. Select your repository
  4. Vercel auto-detects that it's a Next.js project and configures build settings
  5. Click "Deploy"

That's it. Your app is live on a .vercel.app domain within minutes.

Environment Variables

Your .env.local file doesn't get deployed (it's in .gitignore, as it should be). You need to add those variables in the Vercel dashboard:

  1. Go to your project settings
  2. Navigate to "Environment Variables"
  3. Add each variable with its value
  4. Choose which environments it applies to: Production, Preview, or Development

Vercel gives you three environments. Production is your live site. Preview is auto-generated for every pull request. Development is for vercel dev local development.

Preview Deployments

This is one of Vercel's killer features. Every time you open a pull request, Vercel automatically builds and deploys a preview version with its own unique URL. Your team can review the actual running application, not just the code diff. When you merge the PR, the preview is cleaned up automatically.

Build Settings

Most of the time, Vercel's auto-detection works perfectly. If you need to customize:

  • Build Command: npm run build (or next build)
  • Output Directory: .next (auto-detected)
  • Install Command: npm ci
  • Root Directory: Usually . unless you have a monorepo

Custom Domain Setup

A .vercel.app URL works for testing, but for a real product you need a real domain.

Buying a Domain

You can buy domains from registrars like Namecheap, Cloudflare Registrar, or directly through Vercel. Cloudflare is often the cheapest because they sell at cost. Vercel is the most convenient because the DNS configuration is automatic.

Configuring DNS

If you bought your domain elsewhere, you need to point it at Vercel. You have two options:

Option 1: Nameservers (recommended) Change your domain's nameservers to Vercel's. This gives Vercel full control over DNS and makes configuration automatic.

Option 2: DNS Records Add these records at your registrar:

  • A Record: @ pointing to 76.76.21.21
  • CNAME Record: www pointing to cname.vercel-dns.com

Adding the Domain in Vercel

  1. Go to your project settings
  2. Navigate to "Domains"
  3. Enter your domain name
  4. Vercel verifies the DNS configuration
  5. SSL certificate is automatically provisioned

SSL Certificate

Vercel handles SSL automatically using Let's Encrypt. Within minutes of adding your domain, HTTPS is working. No configuration needed, no certificates to renew, no headaches.

WWW Redirect

Decide whether your canonical URL is example.com or www.example.com. Vercel lets you set one as primary and automatically redirects the other. Most modern sites use the bare domain (no www).

Security Headers

Security headers tell browsers how to behave when loading your site. They're your first line of defense against common attacks. Configure them in next.config.js:

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;"
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY'
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  },
  {
    key: 'Referrer-Policy',
    value: 'origin-when-cross-origin'
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()'
  }
];

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ];
  },
};

export default nextConfig;

Here's what each header does:

  • Content-Security-Policy (CSP): Controls which resources the browser can load. Prevents XSS attacks by restricting where scripts can come from.
  • X-Frame-Options: Prevents your site from being embedded in iframes, blocking clickjacking attacks.
  • X-Content-Type-Options: Stops browsers from guessing content types, preventing MIME-type sniffing attacks.
  • Strict-Transport-Security (HSTS): Forces browsers to always use HTTPS, even if someone types http://.
  • Permissions-Policy: Restricts which browser features your site can access (camera, mic, geolocation).

Don't memorize these. Prompt your AI with "add production security headers to my Next.js config" and review what it generates. The key is knowing they exist and that they matter.

Environment Variables Management

Environment variables are the bridge between your code and the outside world — API keys, database URLs, feature flags, and configuration that changes between environments.

The Three Environments

Development (.env.local):

DATABASE_URL="postgresql://localhost:5432/myapp_dev"
STRIPE_SECRET_KEY="sk_test_..."
NEXT_PUBLIC_APP_URL="http://localhost:3000"

Staging (Vercel Preview): Set in Vercel dashboard with the "Preview" environment selected. These are used for pull request preview deployments.

Production (Vercel Production): Set in Vercel dashboard with the "Production" environment selected. These are your real API keys and production database URLs.

The Golden Rules

  1. Never commit secrets. Your .env.local must be in .gitignore. Always.
  2. Use NEXT_PUBLIC_ prefix only for variables that are safe to expose to the browser. Database URLs and API secrets must never have this prefix.
  3. Rotate secrets regularly. If a key is compromised, you should be able to rotate it without a code deployment — just update the environment variable.
  4. Document your variables. Create a .env.example file with placeholder values so new team members know what variables are needed:
# .env.example
DATABASE_URL="postgresql://user:password@host:5432/dbname"
STRIPE_SECRET_KEY="sk_test_..."
NEXT_PUBLIC_APP_URL="http://localhost:3000"

Monitoring and Observability

Deploying is not the finish line — it's the starting line. Once your app is live, you need to know what's happening.

Error Tracking with Sentry

Sentry captures errors in real time and gives you stack traces, user context, and the exact sequence of events that led to the crash.

npx @sentry/wizard@latest -i nextjs

This wizard sets up everything: the SDK, source maps, and the Sentry configuration file. After setup, every unhandled error in your application — client-side or server-side — gets reported to your Sentry dashboard with full context.

The prompt to set it up:

Integrate Sentry error monitoring into my Next.js project.
Set up both client and server error tracking.
Include source maps for readable stack traces.

Vercel Analytics

If you're on Vercel, their built-in analytics give you:

  • Web Vitals: Real-user Core Web Vitals data (LCP, FID, CLS)
  • Page views and visitors: Basic traffic analytics
  • Speed Insights: Performance data broken down by page and device

Enable it in your Vercel dashboard or install the package:

npm install @vercel/analytics
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

Uptime Monitoring

Uptime monitors ping your site at regular intervals and alert you if it goes down. Options include:

  • Better Uptime — beautiful status pages, generous free tier
  • UptimeRobot — simple, reliable, free for up to 50 monitors
  • Checkly — more advanced, supports API monitoring and browser checks

Set up at least one monitor for your homepage and one for your most critical API endpoint. Configure alerts to go to your phone (SMS or push notifications), not just email.

Log Management

For simple applications, Vercel's built-in function logs are sufficient. You can view them in the Vercel dashboard under "Logs." For more complex needs, services like Axiom (which integrates directly with Vercel), Datadog, or LogTail give you searchable, filterable log management.

Performance Optimization

Performance isn't a luxury — it's a feature. Every 100ms of load time improvement increases conversion rates. Google uses page speed as a ranking factor. And users on slow connections will bounce if your site doesn't load quickly.

Image Optimization

Next.js has a built-in Image component that handles optimization automatically:

import Image from 'next/image';

export function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Product hero image"
      width={1200}
      height={630}
      priority // Load this image immediately (above the fold)
      sizes="(max-width: 768px) 100vw, 1200px"
    />
  );
}

The Image component automatically serves WebP or AVIF formats, generates responsive sizes, lazy-loads images below the fold, and prevents layout shift by reserving space. Use the priority prop only for above-the-fold images (hero images, logos).

Font Optimization

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Show fallback font while loading
});

export default function RootLayout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

next/font self-hosts fonts and eliminates the external network request to Google Fonts. The display: 'swap' property ensures text is visible immediately with a fallback font while the custom font loads.

Code Splitting

Next.js automatically code-splits by route, so each page only loads the JavaScript it needs. For heavy components within a page, use dynamic imports:

import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => <div className="h-64 animate-pulse bg-gray-200 rounded" />,
  ssr: false, // Don't render on server if it's client-only
});

This means the Chart component's code is only downloaded when a user visits a page that uses it, not on every page load.

Caching Strategies

  • ISR (Incremental Static Regeneration): Static pages that revalidate on a timer. Great for content that changes hourly or daily.
  • SWR (Stale-While-Revalidate): Show cached data immediately while fetching fresh data in the background. Perfect for dashboards.
  • CDN Cache Headers: Set Cache-Control headers to cache static assets at the CDN edge.
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour

Core Web Vitals

Google measures three key metrics:

  • LCP (Largest Contentful Paint): How fast the main content loads. Target: under 2.5 seconds. Fix by optimizing images, using CDN, and reducing server response time.
  • FID (First Input Delay) / INP (Interaction to Next Paint): How fast your site responds to user interaction. Target: under 200ms. Fix by reducing JavaScript, using code splitting, and avoiding long tasks.
  • CLS (Cumulative Layout Shift): How much the page layout jumps around. Target: under 0.1. Fix by setting image dimensions, reserving space for dynamic content, and avoiding late-loading elements that push content around.

Database in Production

If you've been developing with SQLite (common for local development), production usually means migrating to PostgreSQL or MySQL.

Migrating to PostgreSQL

For a Prisma-based project, the migration is straightforward:

  1. Update your schema: Change the provider in schema.prisma:
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
  1. Set up a PostgreSQL database: Services like Neon, Supabase, or Railway offer free tiers with managed PostgreSQL.

  2. Update your connection string:

DATABASE_URL="postgresql://user:password@host:5432/myapp?sslmode=require"
  1. Run migrations:
npx prisma migrate deploy

Production Database Best Practices

  • Backups: Ensure your database provider has automated daily backups. Test restoring from a backup at least once.
  • Connection Pooling: Serverless functions open many short-lived connections. Use a connection pooler like PgBouncer (built into Neon and Supabase) to avoid overwhelming your database.
  • Migrations: Always use a migration tool (Prisma Migrate, Drizzle Kit). Never modify production databases by hand.
  • Monitoring: Watch for slow queries, connection count, and disk usage. Most managed database providers include a dashboard for this.

What's Next

Your application is deployed, monitored, and performing well. But no one knows it exists yet. In the next lesson, we'll tackle SEO, analytics, and growth — making your application discoverable, tracking what matters, and building an audience without a marketing budget. Organic traffic is the best kind of traffic: free, compounding, and full of people actively searching for what you've built.