Vercel is the deploy story Next.js was designed for, configured to stay predictable
Preview URLs on every PR, edge runtime where it pays off, on-demand revalidation that drops the cache exactly when content changes. We architect the Vercel deployment so the bill is something you plan around, not something you discover.
Why this stack
Deploy is git push, not a CI ritual
A merge to main triggers a build, builds emit immutable deploys, the previous deploy stays live until the new one is ready. There is no CI script to maintain, no Docker image to push, no rollback panic at 11 pm. A bad deploy is one click back to the previous commit.
Preview URLs per PR are the killer feature for client review
Every PR gets a unique URL with the full app running on a copy of the database where you point it. Stakeholders click a link and see the change before merge, designers compare side by side, the back-and-forth that used to take five emails becomes a comment thread on the PR.
Edge runtime where it matters, Node where it belongs
Auth checks, locale rewrites, rate limits and personalisation run at the edge for sub-100ms first byte. Stripe webhooks, file uploads and anything that needs the full Node API stays on the Node runtime. The choice is per route, not per app.
ISR with on-demand revalidation
Pages get rendered once, served from the cache at the edge, regenerated when the content actually changes. A webhook from your CMS triggers `revalidatePath` for the exact route; the cache drops, the next request rebuilds. No nightly cron rebuilds, no cache invalidation guesses.
Cost is predictable when you architect for it
The famous Vercel bill is what happens when a team treats it as a magic box. We design the deployment so cost lives in three places (function invocations, bandwidth, image optimisation), with an alerting threshold on each. The bill becomes a budget line, not a quarterly surprise.
What we build with it
Vercel project setup
Environments (production, preview, development), env vars synced, domain configuration, redirects and security headers in `vercel.json`.
Preview URL workflow
GitHub integration with branch protection, preview comments on PRs, deploy summaries posted to Slack or Linear where the team works.
Edge runtime configuration per route
Routes that hit external APIs and read databases run on Node; routes that only personalise headers or do simple rewrites run at the edge.
ISR + on-demand revalidation
Long TTL caches on content pages, webhook-triggered revalidation tied to the CMS or Supabase, `revalidatePath` and `revalidateTag` in Server Actions.
Image optimisation wired correctly
`next/image` with explicit `sizes`, properly tuned `deviceSizes` and `imageSizes`, remote pattern allowlist, AVIF where it pays off.
Edge Config for feature flags
Sub-millisecond reads of feature flags from anywhere in the app, no extra service to manage, gradual rollout patterns wired in.
Cron jobs
Scheduled tasks defined in `vercel.json` with a deduplicating handler, timeouts set per job, alerting on failure.
Log Drains to your observability stack
Function logs streamed to Datadog, Axiom or your destination of choice, structured logging from the application emitting JSON, request IDs traced end-to-end.
Analytics + Speed Insights
Vercel Analytics for page views, Speed Insights for Core Web Vitals per route, both wired to alerts when a release regresses.
Custom domains, redirects, headers
Multi-domain setup for multi-locale, 301 redirects from the old URLs, CSP and HSTS headers configured in `vercel.json`.
Cost monitoring + alerting
Vercel usage API polled daily, alerts set per category (function execution, bandwidth, image optimisation), monthly trend reports.
Migration from Netlify, Heroku, AWS
Existing deploys mapped to Vercel equivalents, env vars migrated, CI/CD pipelines simplified, old infrastructure deprecated with a cutover plan.
A Vercel-native deploy config with edge, ISR and revalidation in one project
A `vercel.json` defining headers, redirects and crons; a `next.config.ts` with image and cache strategy; a route handler that runs at the edge for auth; a Server Action that calls `revalidatePath` when content changes. Four files; covers the production deploy without secrets.
Most "deploy to Vercel" guides stop at hitting the button. Production Vercel is a config you architect. The four files below are what we ship: a vercel.json with headers, redirects and crons; a next.config.ts with the cache strategy spelled out; an edge route that runs auth in under 50ms of first byte; and a Server Action that revalidates the right path when content changes.
1. The vercel.json config
The production config lives in source control. Headers, redirects, crons and rewrites are reviewable, diffable and tied to the commit that introduced them.
// vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=63072000; includeSubDomains; preload"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
}
]
},
{
"source": "/_next/static/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
],
"redirects": [
{
"source": "/blog/old-slug",
"destination": "/blog/new-slug",
"permanent": true
}
],
"crons": [
{
"path": "/api/cron/reconcile-stripe",
"schedule": "0 3 * * *"
},
{
"path": "/api/cron/refresh-sitemap",
"schedule": "0 4 * * *"
}
]
}
2. The Next.js cache strategy
The next.config.ts makes the cache strategy explicit: ISR for content pages, no caching for authenticated routes, image optimisation tuned to the actual breakpoints the design system uses.
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [360, 640, 768, 1024, 1280, 1536, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
pathname: '/uploads/**',
},
],
},
experimental: {
// Long-lived ISR cache for blog content; bust on demand via webhook.
isrFlushToDisk: true,
},
async headers() {
return [
{
source: '/api/:path*',
headers: [{ key: 'Cache-Control', value: 'no-store' }],
},
]
},
}
export default config
3. The edge route
The auth check runs at the edge in milliseconds. The route reads the session cookie, validates it against a JWT public key (no database round-trip), and either rewrites to the application or redirects to login.
// app/api/auth/check/route.ts
import { NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
export const runtime = 'edge'
const PUBLIC_KEY = process.env.SESSION_PUBLIC_KEY!
export async function GET(request: Request) {
const cookie = request.headers.get('cookie')
const token = cookie?.match(/session=([^;]+)/)?.[1]
if (!token) return NextResponse.json({ ok: false }, { status: 401 })
try {
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(PUBLIC_KEY),
)
return NextResponse.json({ ok: true, userId: payload.sub })
} catch {
return NextResponse.json({ ok: false }, { status: 401 })
}
}
4. On-demand revalidation from a Server Action
When a content editor publishes a post, the Server Action writes to the database and calls revalidatePath for the exact route. The cached version drops; the next request rebuilds and re-caches. No nightly rebuild needed.
// app/admin/posts/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { createServerClient } from '@/lib/supabase/server'
export async function publishPost(postId: string): Promise<{ ok: true } | { ok: false; error: string }> {
const supabase = await createServerClient()
const { data, error } = await supabase
.from('posts')
.update({ status: 'published', published_at: new Date().toISOString() })
.eq('id', postId)
.select('slug, locale')
.single()
if (error) return { ok: false, error: error.message }
// Drop the cache for this exact post and the locale-level index.
revalidatePath(`/${data.locale}/blog/${data.slug}`)
revalidatePath(`/${data.locale}/blog`)
return { ok: true }
}
5. What this buys you
The deploy is a git push. The preview is a URL that shows up on the PR. The auth check answers in 30 milliseconds. The blog cache regenerates the moment an editor presses publish, not on a nightly cron. The bill lands where you expect because every cache decision lives in source control where you can audit it.
The Vercel that gets a bad reputation is the one configured by accident. The Vercel that earns its place is the one architected, with intent, three or four files at a time.
Frequently asked questions
Vercel or self-hosted Node?
Vercel by default for the deploy experience, edge runtime, preview URLs and the Next.js feature parity. Self-hosted on Node when the procurement environment requires on-premise, when egress costs would dominate, or when there is an in-house ops team with capacity. The application code stays portable; only the deploy target changes.
How do you keep the Vercel bill predictable?
Three discipline points. Function execution: keep Server Components fast, avoid heavy work in the request path, push slow tasks to background jobs. Bandwidth: cache pages aggressively, optimise images, use a CDN for large static assets. Image optimisation: explicit sizes everywhere, AVIF where it pays off, remote pattern allowlist tight.
Does the free tier work for production?
For a side project and a small SaaS, often yes. For anything with real traffic, the Pro plan pays for itself in seats, bandwidth and image optimisation included. We model the cost in the scoping phase based on expected traffic, not on optimism.
What about bandwidth at scale?
Vercel's bandwidth pricing is straightforward; the trap is unoptimised assets. We pair Vercel with Cloudflare R2 for large static files, tune `next/image` properly, and cache everything cacheable. Bandwidth becomes a smaller line item than function execution in most projects.
How wide is the edge network?
Vercel's edge runs across multiple regions worldwide. For most projects this is more than enough. For specific compliance reasons (EU data residency on every request) we set explicit regions and document the trade-off.
When do you reach for custom infrastructure?
When the workload is genuinely incompatible with serverless: long-running background jobs, persistent WebSocket connections that outlive a function, heavy memory or GPU usage. In those cases we run the workload on a dedicated host and keep the Next.js application on Vercel.
How long does migration from Netlify or Heroku take?
A week or two for a medium application. The cutover itself is one day with DNS prep beforehand. The bigger work is rewriting CI/CD pipelines (often simpler on Vercel), reorganising the env-var inventory, and re-checking redirects and headers. We do this as a single fixed-scope engagement.
Tell us about your Vercel deployment
A scoping call, a concrete number in the first reply, no agency theater. New deploys and migrations from Netlify, Heroku or AWS both welcome.