Design Engineering
Quality & Polish

Deployment

Ship with confidence using automated CI/CD pipelines, environment-based configuration, preview deployments, rollback strategies, and production monitoring.

Deployment is not the end of development — it is the beginning of the feedback loop. A professional deployment process makes shipping safe, fast, and reversible. No deploy should require manual steps, and every deploy should be observable.

Environment Strategy

Professional frontend projects maintain at minimum three environments: development, preview, and production. Each serves a distinct purpose and should be treated as separate deployment targets with separate configuration.

EnvironmentPurposeWho uses itDeployment trigger
Development (local)Active feature development and debuggingIndividual developerLocal — pnpm dev
PreviewReview new features before merging; share with stakeholders and QADeveloper, reviewer, QA, stakeholdersAutomatic on every pull request
StagingFinal pre-production validation; mirrors production data shapeQA, product ownerAutomatic on merge to main or release branch
ProductionLive application serving end usersEnd usersManual approval gate or automatic on tag

Preview deployments are one of the highest-value practices in modern frontend development. Every PR getting its own isolated URL eliminates the need for a shared staging environment for most review workflows and dramatically reduces feedback cycle time.

Environment Variables

Environment variables are the correct mechanism for all configuration that differs between environments — API URLs, feature flags, third-party service keys. They must never be hardcoded in source code or committed to version control.

Next.js environment variable rules

  • NEXT_PUBLIC_* — exposed to the browser bundle. Use only for non-sensitive config: public API endpoints, analytics IDs, feature flag keys.
  • All other variables — server-only. Never accessible in client components or the browser. Use for database connection strings, secret keys, and private API tokens.
  • .env.local — local developer overrides. Never committed to version control.
  • .env.example — documents required variables with placeholder values. Always committed.
.env.example — document all required variables
# Application
NEXT_PUBLIC_APP_URL=https://your-domain.com
NEXT_PUBLIC_APP_ENV=production

# Database (server-only — never prefix with NEXT_PUBLIC_)
DATABASE_URL=postgresql://user:password@host:5432/dbname

# Authentication
AUTH_SECRET=generate-with-openssl-rand-base64-32
AUTH_URL=https://your-domain.com

# Third-party APIs (server-only)
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Public keys safe for browser exposure
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
Runtime validation of environment variables at startup
// lib/env.ts — fail fast if required variables are missing
import { z } from "zod"

const serverEnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  AUTH_SECRET: z.string().min(32),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
})

const clientEnvSchema = z.object({
  NEXT_PUBLIC_APP_URL: z.string().url(),
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
})

// Throws at build/boot time if any required variable is missing or malformed
export const serverEnv = serverEnvSchema.parse(process.env)

export const clientEnv = clientEnvSchema.parse({
  NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
})

CI/CD Pipeline

A CI/CD pipeline automates the quality gates between a code change and production. The pipeline is the enforcer — it is the reason developers can ship with confidence without manually running every check before each deploy.

  1. Lint and type-check Run ESLint and TypeScript type-checking. These are fast (typically under 30 seconds) and catch the widest range of issues. No further steps run if these fail.

  2. Unit and integration tests Run the full Vitest suite with coverage reporting. Gate E2E tests behind this step — if unit tests are failing, E2E tests are wasted time.

  3. Build Run next build to verify the production bundle compiles without errors. This catches type errors in pages, invalid imports, and build-time data fetching failures.

  4. E2E tests against preview deployment Deploy to a preview environment and run Playwright E2E tests against it. This is the final gate before production — it tests the real application, not a simulated one.

  5. Deploy to production On merge to main (or manual approval for sensitive applications), deploy to production. The deployment itself should be atomic — zero downtime, instant rollback available.

Complete CI/CD pipeline — GitHub Actions
# .github/workflows/ci.yml
name: CI/CD

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

jobs:
  quality:
    name: Lint, type-check & test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check
      - run: pnpm test --coverage
      - run: pnpm build

  e2e:
    name: End-to-end tests
    runs-on: ubuntu-latest
    needs: quality
    if: github.event_name == 'pull_request'
    environment: preview
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm exec playwright install --with-deps chromium
      - run: pnpm exec playwright test
        env:
          BASE_URL: ${{ vars.PREVIEW_URL }}
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

  deploy:
    name: Deploy to production
    runs-on: ubuntu-latest
    needs: quality
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment:
      name: production
      url: https://your-domain.com
    steps:
      - uses: actions/checkout@v4
      - uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: "--prod"

Preview Deployments

Every pull request should automatically deploy to a unique, isolated URL. This enables asynchronous review — designers, product managers, and QA can review changes without a developer being present. It also enables running E2E tests against a real deployment rather than a local server.

For strategies on structuring review-ready PRs, splitting visual from behavioural changes, and coordinating preview deployment sign-off across the team, see Collaborative Repository Workflow.

PlatformPreview URL formatConfig required
Vercelproject-name-git-branch-org.vercel.appZero config for Next.js projects
Netlifydeploy-preview-{PR#}--site-name.netlify.appnetlify.toml with build settings
Cloudflare Pagesbranch-name.project-name.pages.devWrangler config or dashboard setup
Renderservice-name-pr-{PR#}.onrender.comrender.yaml with preview environment settings

Rollback Strategy

Every production deployment must be reversible within minutes. Despite thorough testing, issues will occasionally reach production. The ability to rollback instantly is not optional — it is a requirement for any professional deployment process.

  • Immutable deployments: Every deployment is a distinct, immutable artifact. Rollback means re-activating a previous artifact — not reverting code and redeploying.
  • Deployment history: Maintain a log of all production deployments with the ability to one-click promote any previous deployment to production.
  • Feature flags: For high-risk features, deploy the code behind a feature flag set to off. Enable it for a percentage of traffic and monitor before full rollout. Disable instantly without a deployment.
  • Database migrations: All schema changes must be backward-compatible with the previous version of the application. Never apply a breaking migration and a code deploy simultaneously.

Expand-and-contract is the safe pattern for breaking database changes: first deploy a migration that adds the new column (expand), deploy the new code that writes to both columns, then deploy a migration that removes the old column (contract). Attempting to do this in a single step will cause downtime.

Production Monitoring

Deploying without monitoring is guesswork. You need to know when errors occur, when performance degrades, and which features users are actually using. Set up monitoring before you need it — after an incident is too late.

ConcernWhat to monitorTool
JavaScript errorsUnhandled exceptions, promise rejections, component error boundariesSentry
Performance (Real User Monitoring)LCP, INP, CLS from real users on real devices and connectionsVercel Speed Insights / Datadog RUM
UptimeHTTP 200 responses from key routes on a defined intervalCheckly / UptimeRobot / Grafana
API latency and error ratesp50/p95/p99 response times, 4xx/5xx rates per endpointDatadog / New Relic / Grafana
Product analyticsFeature adoption, funnel conversion, user retentionPostHog / Mixpanel
Log aggregationServer-side logs, build logs, edge function outputVercel Logs / Datadog / Papertrail
Sentry setup in Next.js
// instrumentation.ts — runs on server startup
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { init } = await import("@sentry/nextjs")
    init({
      dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
      environment: process.env.NEXT_PUBLIC_APP_ENV,
      tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
      // Capture 100% of errors but only 10% of traces in production
    })
  }
}

// Error boundary for client-side errors
// app/error.tsx

<Callout type="info">
  For a complete guide to frontend observability — metrics, alerting, incident response, and analytics integration — see [Production Observability](/docs/production-observability).
</Callout>

"use client"
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    Sentry.captureException(error)
  }, [error])

  return (
    <div>
      <h2>Something went wrong</h2>
      <button onClick={reset}>Try again</button>
    </div>
  )
}

On this page