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.
| Environment | Purpose | Who uses it | Deployment trigger |
|---|---|---|---|
| Development (local) | Active feature development and debugging | Individual developer | Local — pnpm dev |
| Preview | Review new features before merging; share with stakeholders and QA | Developer, reviewer, QA, stakeholders | Automatic on every pull request |
| Staging | Final pre-production validation; mirrors production data shape | QA, product owner | Automatic on merge to main or release branch |
| Production | Live application serving end users | End users | Manual 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.
# 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// 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.
-
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.
-
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.
-
Build Run
next buildto verify the production bundle compiles without errors. This catches type errors in pages, invalid imports, and build-time data fetching failures. -
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.
-
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.
# .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.
| Platform | Preview URL format | Config required |
|---|---|---|
| Vercel | project-name-git-branch-org.vercel.app | Zero config for Next.js projects |
| Netlify | deploy-preview-{PR#}--site-name.netlify.app | netlify.toml with build settings |
| Cloudflare Pages | branch-name.project-name.pages.dev | Wrangler config or dashboard setup |
| Render | service-name-pr-{PR#}.onrender.com | render.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.
| Concern | What to monitor | Tool |
|---|---|---|
| JavaScript errors | Unhandled exceptions, promise rejections, component error boundaries | Sentry |
| Performance (Real User Monitoring) | LCP, INP, CLS from real users on real devices and connections | Vercel Speed Insights / Datadog RUM |
| Uptime | HTTP 200 responses from key routes on a defined interval | Checkly / UptimeRobot / Grafana |
| API latency and error rates | p50/p95/p99 response times, 4xx/5xx rates per endpoint | Datadog / New Relic / Grafana |
| Product analytics | Feature adoption, funnel conversion, user retention | PostHog / Mixpanel |
| Log aggregation | Server-side logs, build logs, edge function output | Vercel Logs / Datadog / Papertrail |
// 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>
)
}Animation & Motion
Use animation intentionally — CSS transitions, keyframe animations, Framer Motion, gesture handling, page transitions, and reduced motion support.
Production Observability
Systematic observability for frontend applications — what to observe, how to alert, and how to respond when things go wrong in production.