Next.js course Β· Module 8: Deployment and Production
Preview Deployments and staging environments
In this lesson6
In Quantum Metropolis 2150, no system goes directly to production servers. Every change goes through a rigorous verification process - from preview deployments for individual Pull Requests, through the staging environment, to a controlled promotion to production. Such a pipeline ensures that residents of Quantum City never experience faulty code.
What are Preview Deployments?
Preview Deployments are automatic application deployments generated for each Pull Request. Every PR receives its own, unique URL at which changes can be tested before being merged into the main branch.
Vercel - the platform created by the Next.js authors - offers the best support for Preview Deployments. But other platforms, like Netlify or AWS Amplify, also support this concept.
How do Preview Deployments work on Vercel?
- You create a Pull Request on GitHub
- Vercel automatically detects the new PR
- Builds the application with code from this PR
- Generates a unique URL (e.g.
project-git-feature-xyz-team.vercel.app) - Adds a comment to the PR with a link to the preview
1# Example Preview Deployments URLs:
2# PR #42: my-app-git-feature-login-myteam.vercel.app
3# PR #43: my-app-git-fix-header-myteam.vercel.app
4# PR #44: my-app-git-update-api-myteam.vercel.app
5
6# Each URL is unique and works as long as the PR existsConfiguring Vercel for Preview Deployments
1// vercel.json
2{
3 "git": {
4 "deploymentEnabled": {
5 "main": true,
6 "staging": true
7 }
8 },
9 "github": {
10 "autoAlias": true,
11 "autoJobCancelation": true,
12 "silent": false
13 },
14 "headers": [
15 {
16 "source": "/(.*)",
17 "headers": [
18 {
19 "key": "X-Robots-Tag",
20 "value": "noindex"
21 }
22 ]
23 }
24 ]
25}Important note: Preview Deployments should have the header X-Robots-Tag: noindex so that search engines do not index test versions!
Deployment environments (dev β staging β prod)
Professional projects use a multi-level deployment pipeline:
Hierarchy of environments
1Development (dev) β Developer's local machine
2 β
3Preview β Automatic per-PR (Vercel Preview)
4 β
5Staging β Production mirror (final tests)
6 β
7Production (prod) β Production servers (end users)Branch-based deployments
Each Git branch can be mapped to a different environment:
1# Branch β environment configuration in Vercel:
2# main β Production (app.quantum-city.com)
3# staging β Staging (staging.quantum-city.com)
4# develop β Development (dev.quantum-city.com)
5# feature/* β Preview (auto-generated URL)
6
7# Developer workflow:
8git checkout -b feature/new-dashboard
9# ... coding ...
10git push origin feature/new-dashboard
11# β Automatically: Preview Deployment!
12
13# After review and merge into staging:
14git checkout staging
15git merge feature/new-dashboard
16git push origin staging
17# β Automatically: Staging Deployment!
18
19# After tests on staging, merge into main:
20git checkout main
21git merge staging
22git push origin main
23# β Automatically: Production Deployment!Environment variables per environment
Each environment has its own variables:
1// Vercel CLI - adding variables per environment
2// vercel env add DATABASE_URL development
3// vercel env add DATABASE_URL preview
4// vercel env add DATABASE_URL production
5
6// In the code, we check the environment:
7// app/api/config/route.ts
8import { NextResponse } from 'next/server';
9
10export async function GET() {
11 const environment = process.env.VERCEL_ENV; // 'production' | 'preview' | 'development'
12 const gitBranch = process.env.VERCEL_GIT_COMMIT_REF; // e.g. 'feature/login'
13 const commitSha = process.env.VERCEL_GIT_COMMIT_SHA; // commit hash
14
15 return NextResponse.json({
16 environment,
17 branch: gitBranch,
18 commit: commitSha?.slice(0, 7),
19 // Different databases per environment
20 database: environment === 'production'
21 ? 'prod-cluster'
22 : environment === 'preview'
23 ? 'preview-cluster'
24 : 'dev-cluster',
25 });
26}Protected Deployments
In the staging and preview environments, it's worth protecting access so that unauthorized people do not see unpublished changes:
1// middleware.ts - protecting preview and staging
2import { NextRequest, NextResponse } from 'next/server';
3
4export function middleware(request: NextRequest) {
5 const environment = process.env.VERCEL_ENV;
6
7 // Do not protect production
8 if (environment === 'production') {
9 return NextResponse.next();
10 }
11
12 // Check authorization for preview/staging
13 const authHeader = request.headers.get('authorization');
14 const previewToken = process.env.PREVIEW_ACCESS_TOKEN;
15
16 // Allow access with a token or cookie
17 const hasToken = authHeader === 'Bearer ' + previewToken;
18 const hasCookie = request.cookies.get('preview-access')?.value === previewToken;
19
20 if (!hasToken && !hasCookie) {
21 // Redirect to the preview login page
22 return NextResponse.redirect(new URL('/preview-login', request.url));
23 }
24
25 return NextResponse.next();
26}
27
28export const config = {
29 matcher: ['/((?!preview-login|api|_next/static|favicon.ico).*)'],
30};Deployment Hooks - deployment automation
Deploy Hooks are webhook URLs that trigger a new deployment when called:
1# Creating a Deploy Hook in Vercel:
2# Settings β Git β Deploy Hooks
3# Name: "CMS Content Update"
4# Branch: main
5# β Generates URL: https://api.vercel.com/v1/integrations/deploy/prj_xxx/yyy
6
7# Usage with CMS (e.g., after publishing an article):
8curl -X POST https://api.vercel.com/v1/integrations/deploy/prj_xxx/yyy
9
10# Usage in GitHub Actions:
11# - name: Trigger Vercel Deploy
12# run: curl -X POST ${{ secrets.VERCEL_DEPLOY_HOOK }}
13
14# Usage with Strapi/Sanity webhook:
15# After publishing content β POST to Deploy Hook URL
16# β Vercel rebuilds the page with new data (ISR/SSG)Practical workflow with Deploy Hooks
1// app/api/webhook/cms/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4export async function POST(request: NextRequest) {
5 // Verifying the webhook from the CMS
6 const signature = request.headers.get('x-webhook-signature');
7 const secret = process.env.CMS_WEBHOOK_SECRET;
8
9 if (!verifySignature(signature, secret)) {
10 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
11 }
12
13 // Trigger revalidation or new deploy
14 const deployHookUrl = process.env.VERCEL_DEPLOY_HOOK;
15
16 if (deployHookUrl) {
17 await fetch(deployHookUrl, { method: 'POST' });
18 return NextResponse.json({ status: 'Deploy triggered' });
19 }
20
21 return NextResponse.json({ status: 'No deploy hook configured' });
22}
23
24function verifySignature(signature: string | null, secret: string | undefined): boolean {
25 if (!signature || !secret) return false;
26 // HMAC verification implementation
27 return true;
28}Environment Promotion (dev β staging β prod)
Environment promotion is the controlled process of moving code between environments:
1# Strategy 1: Git-flow with automatic deploy
2# develop β auto-deploy to dev
3# staging β auto-deploy to staging
4# main β auto-deploy to production
5
6# Merge workflow:
7git checkout staging
8git merge develop # Promote dev β staging
9git push origin staging # Auto-deploy to staging
10
11# After tests on staging:
12git checkout main
13git merge staging # Promote staging β prod
14git push origin main # Auto-deploy to production
15
16# Strategy 2: Vercel Promote (without merge)
17# Vercel allows promoting an existing deployment:
18vercel promote [deployment-url] --scope=team
19
20# This moves EXACTLY the same build to production
21# Without rebuilding - guarantee of identity!Summary
Preview Deployments and staging environments are the foundations of a professional deployment process:
- Preview Deployments - automatic per-PR deployments, ideal for code review
- Branch-based deployments - mapping branches to environments (dev/staging/prod)
- Environment promotion - controlled flow of code between environments
- Protected Deployments - protection of preview and staging from unauthorized access
- Deploy Hooks - automatic triggering of deployments from CMS or CI/CD
In Quantum Metropolis 2150, no system goes to production without the full preview β staging β production cycle. This guarantees the stability and quality of services for all residents of the cyberpunk city.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Demo: Preview Deployments and staging environments
4interface Deployment {
5 id: string;
6 branch: string;
7 env: 'production' | 'staging' | 'preview';
8 url: string;
9 status: 'ready' | 'building' | 'error';
10 pr?: number;
11 commit: string;
12 age: string;
13}
14
15const deployments: Deployment[] = [
16 { id: '1', branch: 'main', env: 'production', url: 'app.quantum-city.com', status: 'ready', commit: 'a1b2c3d', age: '2h ago' },
17 { id: '2', branch: 'staging', env: 'staging', url: 'staging.quantum-city.com', status: 'ready', commit: 'e4f5g6h', age: '1h ago' },
18 { id: '3', branch: 'feature/new-dashboard', env: 'preview', url: 'app-git-feature-new-dashboard-team.vercel.app', status: 'ready', pr: 42, commit: 'i7j8k9l', age: '30m ago' },
19 { id: '4', branch: 'fix/header-layout', env: 'preview', url: 'app-git-fix-header-layout-team.vercel.app', status: 'building', pr: 43, commit: 'm0n1o2p', age: '5m ago' },
20 { id: '5', branch: 'feature/auth-update', env: 'preview', url: 'app-git-feature-auth-team.vercel.app', status: 'error', pr: 44, commit: 'q3r4s5t', age: '15m ago' },
21];
22
23const envColors: Record<string, string> = { production: '#4caf50', staging: '#ff9800', preview: '#7c3aed' };
24const statusColors: Record<string, string> = { ready: '#4caf50', building: '#2196f3', error: '#f44336' };
25
26const pipeline = [
27 { name: 'Development', desc: 'Local machine', icon: '1' },
28 { name: 'Preview', desc: 'Auto per PR', icon: '2' },
29 { name: 'Staging', desc: 'Final tests', icon: '3' },
30 { name: 'Production', desc: 'Users', icon: '4' },
31];
32
33export default function PreviewDeploymentsDemo() {
34 const [filter, setFilter] = useState<'all' | 'production' | 'staging' | 'preview'>('all');
35 const [showPipeline, setShowPipeline] = useState(true);
36
37 const filtered = filter === 'all' ? deployments : deployments.filter(d => d.env === filter);
38
39 return (
40 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
41 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Preview Deployments</h1>
42 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Pipeline: dev -> preview -> staging -> production</p>
43
44 {showPipeline && (
45 <div style={{ display: 'flex', gap: '4px', marginBottom: '24px', alignItems: 'center' }}>
46 {pipeline.map((step, i) => (
47 <React.Fragment key={step.name}>
48 <div style={{
49 flex: 1, padding: '12px', borderRadius: '8px', textAlign: 'center',
50 background: '#1a1a2e', border: '1px solid #333',
51 }}>
52 <div style={{ color: '#64ffda', fontWeight: 'bold', fontSize: '20px' }}>{step.icon}</div>
53 <div style={{ color: '#fff', fontSize: '13px', fontWeight: 'bold' }}>{step.name}</div>
54 <div style={{ color: '#8892b0', fontSize: '11px' }}>{step.desc}</div>
55 </div>
56 {i < pipeline.length - 1 && <span style={{ color: '#64ffda', fontSize: '20px' }}>→</span>}
57 </React.Fragment>
58 ))}
59 </div>
60 )}
61
62 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
63 {(['all', 'production', 'staging', 'preview'] as const).map(f => (
64 <button key={f} onClick={() => setFilter(f)} style={{
65 padding: '6px 12px', borderRadius: '6px', border: 'none', cursor: 'pointer', fontSize: '13px',
66 background: filter === f ? (f === 'all' ? '#64ffda' : envColors[f]) : '#1a1a2e',
67 color: filter === f ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
68 }}>{f === 'all' ? 'All' : f.charAt(0).toUpperCase() + f.slice(1)}</button>
69 ))}
70 <button onClick={() => setShowPipeline(!showPipeline)} style={{
71 padding: '6px 12px', borderRadius: '6px', border: '1px solid #333', background: 'transparent', color: '#8892b0', cursor: 'pointer', fontSize: '13px', marginLeft: 'auto',
72 }}>{showPipeline ? 'Hide pipeline' : 'Show pipeline'}</button>
73 </div>
74
75 <div style={{ display: 'grid', gap: '8px' }}>
76 {filtered.map(d => (
77 <div key={d.id} style={{
78 padding: '14px', borderRadius: '8px', background: '#1a1a2e',
79 border: '1px solid ' + envColors[d.env] + '40',
80 }}>
81 <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
82 <span style={{ width: '8px', height: '8px', borderRadius: '50%', background: statusColors[d.status], flexShrink: 0 }} />
83 <code style={{ color: '#fff', fontWeight: 'bold', fontSize: '13px' }}>{d.branch}</code>
84 <span style={{ fontSize: '10px', padding: '2px 8px', borderRadius: '4px', background: envColors[d.env] + '20', color: envColors[d.env] }}>{d.env}</span>
85 {d.pr && <span style={{ fontSize: '11px', color: '#8892b0' }}>PR #{d.pr}</span>}
86 <span style={{ fontSize: '11px', color: '#666', marginLeft: 'auto' }}>{d.age}</span>
87 </div>
88 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
89 <code style={{ color: '#8892b0', fontSize: '12px' }}>{d.url}</code>
90 <code style={{ color: '#666', fontSize: '11px', marginLeft: 'auto' }}>{d.commit}</code>
91 </div>
92 </div>
93 ))}
94 </div>
95 </div>
96 );
97}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Preview Deployments on Vercel are generated automatically:
2. CDN (Content Delivery Network) provides:
These are 2 of 11 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Vertical ordering
Arrange the correct order of environments in the deployment pipeline:
- Vertical ordering
Arrange the elements in the correct order: Define experiment β Split traffic β Track metrics
- Vertical ordering
Arrange the bundle size analysis steps in the correct order:
- Vertical ordering
Arrange the elements in the correct order: Audit dependencies β Set CSP headers β Enable HTTPS
- Vertical ordering
Arrange the elements in the correct order: Code commit to repository β CI/CD pipeline trigger
- Horizontal ordering
Arrange the ESLint configuration in .eslintrc.json
- Vertical ordering
Arrange the stages of production incident handling in the correct order
- Click in order
Arrange the Docker build command:
- Code editor
Write a production deployment checklist in Markdown with five sections (headings): security, performance, monitoring, backup and compliance. Add at least two checkbox items (- [ ] ...) to each section.
- Click in order
Arrange the Vercel deploy configuration: