1. Production Builds & Environment Variables
npm run dev serves unminified code with hot reloading and verbose error
overlays — none of that belongs in front of real users. A production build strips all
of it, minifies and tree-shakes the bundle, and produces the static assets your host
actually serves.
npm run build
Route (app) Size First Load JS
┌ ○ / 142 B 87.3 kB
├ ○ /products 1.2 kB 92.1 kB
└ ƒ /products/[productId] 891 B 88.9 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
The route table from Week 19's next build doubles as a production sanity
check — confirm the routes you expect to be static (○) actually are, before anything
ships. A Vite SPA's equivalent is npm run build, which outputs a
dist/ folder of hashed, minified static assets ready to serve from any
static host.
Environment variables split into two categories, and the difference is a safety
boundary, not a naming convention. A plain DATABASE_URL stays on the
server — never bundled into the JavaScript sent to the browser. Prefixing a variable
with NEXT_PUBLIC_ (Next.js) or VITE_ (Vite) is a deliberate,
explicit opt-in: it tells the build tool "yes, inline this value into the client
bundle, where anyone can read it via DevTools." Forgetting the prefix means the
variable silently stays undefined in the browser; adding it to something
that shouldn't be public is the actual danger.
# server-only -- never sent to the browser, safe for real secrets
DATABASE_URL=postgres://prod-user:***@db.internal:5432/app
STRIPE_SECRET_KEY=sk_live_***
SENTRY_AUTH_TOKEN=***
# client-exposed -- inlined into the JS bundle, readable by anyone
NEXT_PUBLIC_API_BASE_URL=https://api.example.com
NEXT_PUBLIC_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
.env.production above is illustrative — actual secret values belong in your host's environment variable dashboard (Vercel/Netlify project settings), not in a file that gets committed. Keep .env* in .gitignore and commit only a .env.example with placeholder values, so a leaked git history never doubles as a leaked credential dump.
This is also the config file where Week 3's React Compiler gets turned on for a real production build — a one-line addition that makes every eligible component in the app compiler-optimized, without touching component code:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: true, // enables the React Compiler for the whole app's production build
};
export default nextConfig;
After enabling it, next build's output includes compiler diagnostics for
any component that couldn't be optimized (the "bailed out" case from Week 3) — worth a
scan on a real production build, since a component silently missing optimization in a
large app is easy to overlook without checking the build log directly.
2. Deploying to Vercel or Netlify
Both platforms work the same way at a high level: connect a GitHub repository, tell them how to build your app, and every push to your default branch triggers a new production deploy automatically — no manual upload step, no SSH-ing into a server.
Connecting a repo is a few clicks in either dashboard — "Import Project" (Vercel) or "Add new site from Git" (Netlify) — after which the platform detects your framework and pre-fills sensible build settings. It's worth knowing what those settings actually mean, since a Next.js app and a Vite SPA need different values.
Build command: next build
Output directory: .next (Vercel reads this automatically; framework-managed)
Install command: npm install
Build command: npm run build
Output directory: dist (plain static files -- any static host can serve these)
Install command: npm install
A Next.js app needs a platform that understands its server runtime (Vercel is built by the Next.js team specifically for this); a Vite SPA is just static HTML/JS/CSS and can be served by literally any static host, Vercel and Netlify included. Config files let you pin these settings in the repo instead of the dashboard, which keeps deploy behavior reviewable in a pull request like any other code change.
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
The rewrite/redirect rule matters for a Vite SPA using Week 6's client-side router:
without it, refreshing the browser on /products/42 asks the static host
for a file at that literal path, which doesn't exist — the host needs to be told to
serve index.html for every path and let the router handle it client-side.
3. Preview Deployments & Branch Workflows
Beyond the production deploy on your default branch, both platforms deploy every pull request to its own unique, shareable URL — built from that branch's exact code, updated automatically on every new commit pushed to it.
my-app-git-fix-cart-total-yourteam.vercel.app
This changes the code-review workflow in a real way: a reviewer doesn't have to read a diff and imagine how the UI change looks or behaves — they click a link and use the actual running app, with the actual change applied, including anything that's hard to see in a diff (a layout shift, an animation, a genuinely broken interaction). It also means designers and non-engineers who could never review a diff can weigh in directly on the real thing, before it reaches production.
Once a pull request is approved and merged into the default branch, the platform automatically promotes that exact build to production — the same artifact that was being reviewed at the preview URL, not a fresh rebuild that could behave differently. Vercel additionally lets you manually "Promote to Production" any past deployment (preview or production) straight from the dashboard, independent of a git merge — useful during Section 6's rollback discussion.
A preview deployment usually shares environment variables with production by default — worth deliberately pointing preview builds at a staging database or a test Stripe key, so a reviewer clicking around a preview URL can't accidentally mutate real production data.
4. CI Pipeline with GitHub Actions
A host's automatic build-on-push is not the same thing as CI. It happily deploys code that fails your Week 14 test suite or has broken lint rules — it only knows whether the build compiled, not whether the app actually works. A dedicated CI workflow runs alongside it, gating merges on lint, tests, and a build all passing first.
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lint-test-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test -- --run # Vitest (Week 14), non-watch mode in CI
- name: Build
run: npm run build
Each step runs in order, and a non-zero exit code from any step — a failing ESLint rule, a failing Vitest assertion, a build error — halts the job and marks it red on the pull request. Branch protection rules (in GitHub's repo settings) can then require this check to pass before the "Merge" button is even clickable, which is what actually makes this a gate rather than just a notification someone might ignore.
A CI step that logs a warning and exits 0 anyway provides zero actual protection — nothing stops a red-but-ignorable check from merging. The entire value of CI comes from a failing step returning a genuinely non-zero exit code, which is what blocks the merge button when branch protection is configured to require it.
5. Error Monitoring with Sentry
A bug that throws in a user's browser produces an error in their console — invisible to you unless they screenshot it and file a support ticket, which almost never happens. Error monitoring closes that gap: it captures the error, the stack trace, and the state of the app when it happened, and reports it back to you automatically.
npm install @sentry/react
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN, // client-exposed by design -- a DSN isn't a secret
environment: import.meta.env.MODE, // "production" vs "development"
tracesSampleRate: 0.1, // sample 10% of transactions, not every single one
});
For a Next.js app, @sentry/nextjs's setup wizard
(npx @sentry/wizard@latest -i nextjs) wires up equivalent config files for
both the server and client runtimes, since Week 18's server/client split means errors
can originate on either side. Once initialized, an uncaught error anywhere in the
component tree — including inside a Week 13 error boundary's onError
callback, wired up explicitly — is captured with a full stack trace, browser/OS
details, and (if configured) the logged-in user, letting you reproduce and fix issues
you'd otherwise never even know occurred.
6. Rollbacks & Release Safety
CI and monitoring reduce how often something bad reaches production; they don't eliminate it. The remaining question is how fast you can undo damage once it's live, and that's a deploy-level operation, not a git-level one.
Vercel and Netlify both keep every past deployment around and let you instantly
re-point production traffic at any previous one, directly from the dashboard — no
rebuild, no waiting on CI, live again in the time it takes to click a button. This is
meaningfully faster than git revert plus a fresh push-and-rebuild cycle,
and it's the correct first move when something is actively broken for users right now.
function CheckoutPage() {
const newFlowEnabled = useFeatureFlag('new-checkout-flow');
return newFlowEnabled ? <NewCheckoutFlow /> : <LegacyCheckoutFlow />;
}
A feature flag lets you disable just the broken piece of a release without rolling back everything else that shipped alongside it — useful when only one feature in a larger deploy is misbehaving, and reverting the whole deploy would also undo unrelated, working changes. Flags cost more up front (every flagged feature needs the conditional logic and, eventually, cleanup once it's fully rolled out) but buy much finer-grained control when something does go wrong.
1) Check Sentry for a spike in a specific error, not just "something feels off." 2) Confirm which deployment introduced it by comparing the error's first-seen timestamp against your deploy history. 3) Roll back that deployment immediately if users are affected right now — investigate the root cause afterward, not before, since "stop the bleeding" and "understand the bug" are separate steps with different urgency.
7. Hands-on Exercise
Ship a real app: hosting, CI, monitoring, and a rollback plan
Take an app you've already built in this course and put the full production pipeline around it.
Requirements:
- Push the app to a GitHub repository and connect it to Vercel or Netlify, confirming a production deploy succeeds and that any secret values live in the host's environment variable settings, not in the repo.
- Open a pull request with a small, visible change and confirm a preview deployment URL is generated automatically for it; click through the preview to verify the change is actually live there.
- Add a
.github/workflows/ci.ymlfollowing Section 4, running lint, your Week 14 Vitest suite, and a production build on every pull request — then deliberately break a test to confirm the check goes red and (if you enable branch protection) blocks the merge. - Install and initialize Sentry (or an equivalent) with a minimal client-side config, and confirm a deliberately thrown test error actually shows up in the dashboard.
- Write, as a short markdown checklist inside your repo (e.g.
ROLLBACK.md), the exact steps you'd personally take to roll back a bad deploy — from noticing the problem to production traffic pointing at the previous working deployment again. - If your app is a Next.js project, enable
reactCompiler: true, runnext build, and check the output for any compiler diagnostics; if it's a Vite SPA, installbabel-plugin-react-compilerper Week 3 instead. Either way, confirm at least one component compiles cleanly with the optimization applied.
Deliberately breaking a test and watching the CI check actually go red is the only way to confirm the gate works — a workflow file that's never seen a real failure might be silently misconfigured (wrong script name, wrong working directory) and passing green for the wrong reason.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why do client-exposed environment variables need an explicit prefix like NEXT_PUBLIC_ or VITE_, rather than every variable being available by default?
Why do client-exposed environment variables need an explicit prefix like NEXT_PUBLIC_ or VITE_, rather than every variable being available by default?
The prefix is a deliberate safety boundary: it's the explicit act of opting a specific value into the client bundle, where anyone can read it via DevTools. Making every variable available by default would mean a genuine secret (a database password, a private API key) could end up shipped to the browser simply by existing in .env, with no separate step required to catch that mistake.
Q2
What is a preview deployment, and why does it change the code-review workflow?
What is a preview deployment, and why does it change the code-review workflow?
It's a real, running deployment of a pull request's exact branch, built automatically and given its own shareable URL. It changes review because a reviewer can click the link and interact with the actual app as changed, rather than reading a diff and imagining how it looks or behaves — catching visual and interactive issues a diff alone can't show.
Q3
Why should a CI pipeline fail the build on a lint or test failure, rather than just logging a warning and continuing?
Why should a CI pipeline fail the build on a lint or test failure, rather than just logging a warning and continuing?
A step that warns but still exits 0 provides no actual protection — nothing stops the pull request from merging regardless. Only a genuinely non-zero exit code, paired with a branch protection rule requiring that check to pass, actually blocks a broken change from being merged, which is the entire point of putting checks in CI in the first place.
Q4
When you "roll back" a deployment on Vercel or Netlify, what exactly gets reverted?
When you "roll back" a deployment on Vercel or Netlify, what exactly gets reverted?
It re-points production traffic at a previous, already-built deployment — not a git-level operation like git revert, and it doesn't change your repository's commit history at all. That's what makes it fast: there's no rebuild and no waiting on CI, since the previous deployment's build artifact already exists and is simply served again.