Deploy Next.js to Vercel with Edge Middleware and Per-PR Preview Environments
Wire up auth redirects at the edge, scope secrets per environment, and get a fresh preview URL for every pull request, automatically.
What You'll Build
You'll deploy a Next.js app to Vercel with edge middleware that enforces auth and handles redirects at the CDN layer, environment-scoped secrets that differ between production and preview, and a GitHub integration that spins up a unique preview URL automatically for every pull request.
Prerequisites
- Node.js 18.17+ (
node --versionto confirm; Next.js 14 requires this minimum) - A Vercel account at vercel.com (free tier works)
- A GitHub repository for your project
- Vercel CLI:
npm install -g vercel@latest - Familiarity with Next.js App Router basics
1. Bootstrap and Link the Project
If you're starting fresh:
npx create-next-app@latest my-app --typescript --app --tailwind --no-src-dir
cd my-app
The --app flag enables the App Router. The --no-src-dir flag keeps middleware.ts at the project root, which is exactly where Next.js expects it. Skip --tailwind if you don't want it.
Link the project to Vercel:
vercel link
Follow the prompts to connect your account and either create a new Vercel project or attach to an existing one. This writes .vercel/project.json locally, which is already covered by .gitignore.
2. Write Edge Middleware
Create middleware.ts at the project root (same level as package.json):
import { NextRequest, NextResponse } from 'next/server'
const PROTECTED = ['/dashboard', '/settings']
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Auth gate for protected routes
if (PROTECTED.some(path => pathname.startsWith(path))) {
const token = request.cookies.get('session')?.value
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', pathname)
return NextResponse.redirect(loginUrl)
}
}
// Redirect a legacy path permanently
if (pathname === '/old-pricing') {
return NextResponse.redirect(new URL('/pricing', request.url), { status: 301 })
}
// Attach environment context as a response header (visible to the browser)
const response = NextResponse.next()
response.headers.set('x-vercel-env', process.env.VERCEL_ENV ?? 'development')
return response
}
export const config = {
matcher: [
'/dashboard/:path*',
'/settings/:path*',
'/old-pricing',
],
}
The matcher array is important. Without it, middleware runs on every request including /_next/static and image assets, which wastes edge compute. Edge Middleware runs on V8 isolates at Vercel's edge network, so Node.js-specific modules (fs, crypto, jsonwebtoken) are off-limits. For actual JWT verification, use the jose library, which is built on the Web Crypto API and runs fine in the edge runtime.
process.env.VERCEL_ENV is injected automatically by Vercel with values production, preview, or development.
Response headers vs. request headers. response.headers.set() sends that header back to the browser as a response header. Good for debugging in DevTools, but it does not propagate to Server Components or Route Handlers, which read from request headers. If you need x-vercel-env available inside a Server Component, forward it as a modified request header instead:
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-vercel-env', process.env.VERCEL_ENV ?? 'development')
return NextResponse.next({ request: { headers: requestHeaders } })
Then read it in any Server Component via headers().get('x-vercel-env') from next/headers. In Next.js 15, headers() is async, so await it there.
3. Scope Secrets Per Environment
Vercel splits environment variable scopes into three buckets: Production, Preview, and Development. You can manage these from the dashboard under Project Settings > Environment Variables, or via the CLI:
# Production only
vercel env add DATABASE_URL production
# Preview deployments (can point at a staging DB)
vercel env add DATABASE_URL preview
# Local development
vercel env add DATABASE_URL development
Each command prompts for the value interactively. To add the same value across multiple environments at once, omit the environment argument and select from the checklist.
Pull development variables into a local .env.local file:
vercel env pull .env.local
This file is gitignored by default. Never commit it.
Public vs. private: prefix with NEXT_PUBLIC_ only for values safe to expose to the browser. Everything else stays server-side only.
For a truly isolated stack per PR, such as a separate database, Vercel integrates with providers like Neon and PlanetScale that support database branching. Each preview deployment picks up a branch-specific connection string through the integration automatically.
4. Connect GitHub for Per-PR Previews
In the Vercel dashboard, open your project and go to Settings > Git. Connect your GitHub repository. Vercel installs a GitHub App that hooks into push and pull request events.
Once connected:
- Every push to your production branch (
mainby default) triggers a production deployment. - Every push to any other branch, and every PR, gets a unique preview URL like
my-app-git-feature-login-acme.vercel.app.
The Vercel bot posts that URL as a PR comment automatically. No workflow YAML needed.
Preview deployments use your Preview-scoped env vars by default. If you need per-branch overrides, set a Preview env var in the dashboard and click "Add branch" to target a specific branch name.
To restrict preview URLs to your team, enable Vercel Authentication under Project Settings > Deployment Protection. Anyone hitting a preview URL will need to authenticate with your Vercel team before they can see anything.
Verify It Works
-
Push your code and open a PR against
main. The Vercel bot should comment within 30-60 seconds with a preview URL. -
Test the auth redirect. Visit
<preview-url>/dashboardin a fresh browser session with nosessioncookie. You should be redirected to/login?from=/dashboard. That confirms the auth gate works. -
Test the response header. The
x-vercel-envheader is only appended inside theNextResponse.next()path. Redirected requests return early and never reach it, so you won't see the header on the/loginredirect response. To observe it, you need to bypass the auth check by setting a dummy cookie first. In Chrome DevTools, open the Application tab, select Cookies for your preview domain, and add a cookie namedsessionwith any non-empty value (for example,test). Now navigate to<preview-url>/dashboard. In the Network tab, click the/dashboardrequest and look at the Response Headers panel. You should seex-vercel-env: preview. -
In the Vercel dashboard, open the deployment and check the Middleware logs under the Functions tab to confirm it ran.
-
Run
vercel env lsto audit which variables are assigned to which environments.
Expected output from step 5:
name value environments git branch
DATABASE_URL [aes] Production -
DATABASE_URL [aes] Preview -
DATABASE_URL [aes] Development -
Troubleshooting
Middleware not running. Confirm middleware.ts is at the project root, not inside app/ or src/. Also verify the matcher patterns actually match the paths you're testing.
process.env.MY_VAR is undefined in middleware. Make sure the variable is added to the Preview (or Production) scope in Vercel. Environment variable changes don't trigger a redeploy automatically; you need to redeploy after adding them.
Preview URL showing stale content. Check whether your route handlers set aggressive Cache-Control headers. Use the dashboard's cache invalidation option, or confirm your dynamic routes opt out of caching with export const dynamic = 'force-dynamic'.
Redirect loop. The destination of your redirect is probably also matched by the matcher. Exclude the /login path from your PROTECTED array, or narrow the matcher patterns so the redirect target isn't caught.
Next Steps
- Add real JWT validation using jose, which works in the edge runtime and supports RS256/ES256 out of the box.
- Try Vercel Edge Config for near-zero-latency feature flags your middleware can read without an external API call.
- Look at Neon's Vercel integration for per-PR database branches that spin up and tear down alongside preview deployments.
- Tighten preview access further with Deployment Protection and shareable bypass tokens for external reviewers.
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 0
No comments yet
Be the first to weigh in.