How I rotate Sanity draft mode secrets at the edge without redeploying Next.js
Jun 04, 2026 · 6 min read
Sanity draft mode preview on Next.js App Router works fine when your secret is baked into an environment variable — until a client accidentally pastes the preview URL into a Slack channel and you have to redeploy to rotate it. Vercel Edge Config solves that: you update a single key in the dashboard and the new secret is live in under a second, no rebuild required. Here is exactly how I wire it up.
Why the standard env-var approach breaks down
The Next.js docs show a route handler that reads SANITY_PREVIEW_SECRET from process.env and sets the draft-mode cookie. That works for a single developer. It breaks down in two ways on real projects:
- Secret leaks are sticky. Rotating a
process.envvalue means a new Vercel deployment, which takes 60–90 seconds even with Turbopack. During that window the old secret still works on the live deployment. - You cannot audit access. Every stakeholder shares one secret. You cannot tell which link a leaked URL came from.
Edge Config is a key-value store that is distributed to Vercel's edge network. Reads add roughly 1–2 ms of latency and require no cold start because the data is pushed to the edge ahead of the request. Updating a value takes effect globally in under a second without a deploy.
Setting up Edge Config
In the Vercel dashboard: Storage → Create → Edge Config. Add one key:
SANITY_PREVIEW_SECRET = "<random 32-char string>"Then connect the store to your project (Settings → Integrations on the store page). Vercel injects EDGE_CONFIG as a connection string environment variable automatically.
Install the SDK:
npm install @vercel/edge-configThe preview route handler
Create app/api/preview/route.ts. This runs on the Node.js runtime by default; I keep it there rather than the edge runtime so I can use the full Next.js draftMode() API without any caveats.
// app/api/preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { NextRequest, NextResponse } from 'next/server'
import { get } from '@vercel/edge-config'
export async function GET(req: NextRequest) {
const { searchParams } = req.nextUrl
const incomingSecret = searchParams.get('secret')
const slug = searchParams.get('slug') ?? '/'
// 1. Read the current secret from Edge Config — ~1-2 ms, no redeploy needed
const validSecret = await get<string>('SANITY_PREVIEW_SECRET')
if (!incomingSecret || incomingSecret !== validSecret) {
return new NextResponse('Invalid preview secret', { status: 401 })
}
// 2. Validate the slug looks like a real path before redirecting
if (!/^\/[a-z0-9/_-]*$/i.test(slug)) {
return new NextResponse('Invalid slug', { status: 400 })
}
// 3. Enable draft mode — Next.js sets __prerender_bypass cookie (HttpOnly, SameSite=None, Secure)
const draft = await draftMode()
draft.enable()
// 4. Redirect to the requested page
redirect(slug)
}To disable preview, add a matching disable handler:
// app/api/disable-preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET() {
const draft = await draftMode()
draft.disable()
redirect('/')
}Reading draft mode in a page and avoiding content leaks
This is where most implementations go wrong. If you forget the draftMode() check and unconditionally fetch from the Sanity CDN with perspective: 'previewDrafts', you will serve draft content to every visitor whenever the draft-mode cookie is absent but the GROQ query still runs the wrong perspective.
// app/[slug]/page.tsx (simplified)
import { draftMode } from 'next/headers'
import { client } from '@/lib/sanity/client'
const query = `*[_type == "post" && slug.current == $slug][0]{ _id, title, body }`
export default async function PostPage({ params }: { params: { slug: string } }) {
const { isEnabled } = await draftMode()
const post = await client
.withConfig({
// perspective switches between published CDN and draft API
perspective: isEnabled ? 'previewDrafts' : 'published',
// draft fetches must bypass the CDN so editors see live changes
useCdn: !isEnabled,
// only pass the token when actually in draft mode
token: isEnabled ? process.env.SANITY_API_READ_TOKEN : undefined,
})
.fetch(query, { slug: params.slug })
if (!post) return <div>Not found</div>
return <article>{post.title}</article>
}Key constraints:
perspective: 'published'+useCdn: true— fast, cached, no token needed for published content.perspective: 'previewDrafts'+useCdn: false— bypasses CDN, hits the Sanity API directly, requires a read token scoped to draft access.- The read token (
SANITY_API_READ_TOKEN) lives in Vercel environment variables, not Edge Config. It is a long-lived credential; the preview secret is what you rotate frequently.
Cookie security and same-site issues
Next.js sets the __prerender_bypass cookie with SameSite=None; Secure. This means it only works over HTTPS. Local development over http://localhost will silently fail. Fix this by either:
- Using
https://localhostvia a tunnelling tool (ngrok, Cloudflare Tunnel), or - Running
next dev --experimental-httpswhich generates a self-signed cert automatically.
Also, the bypass cookie has no expiry by default — it persists for the browser session. If you want it to expire after, say, 2 hours for security, you cannot do that from the route handler directly (Next.js owns the cookie). The practical workaround: issue your own short-lived session cookie alongside it and check both in middleware.
Secret rotation in production
To rotate the secret: open Vercel → Storage → your Edge Config store → edit SANITY_PREVIEW_SECRET. Change the value, save. Within one second, all preview links using the old secret return 401. No deployment, no downtime for public visitors, no cache flush required.
For multi-client setups I sometimes store an array of secrets and accept any of them with a short overlap window, then remove the old one 30 minutes later. That gives editors time to finish a session after rotation without getting locked out mid-review.
Wiring Sanity Studio to the preview URL
In your Sanity Studio config, set the preview URL to point at your route handler:
// sanity.config.ts
import { defineConfig } from 'sanity'
import { presentationTool } from 'sanity/presentation'
export default defineConfig({
// ...
plugins: [
presentationTool({
previewUrl: {
previewMode: {
enable: '/api/preview',
},
},
}),
],
})The Presentation tool appends ?secret=...&slug=... automatically when editors click the eye icon, so you do not need to hard-code secrets in the Studio config.
The pattern above — Edge Config for the rotating secret, a scoped read token for the Sanity API, and a strict perspective check in every page — is the combination that keeps draft content off your public CDN while still giving editors a fast, reliable preview link they can share with stakeholders without you having to babysit deployments.
Related posts
All posts →How I set up Sanity draft mode preview with Next.js App Router and Vercel Edge Config
May 27, 2026 · 6 min read
Step-by-step: route handlers, cookie strategy, and Vercel Edge Config secret rotation for Sanity draft mode preview in Next.js. No leaks.
How I Handle Sanity Draft Mode Without Sacrificing Edge Performance
Apr 27, 2026 · 4 min read
A production pattern for Sanity draft previews in Next.js that keeps published pages on the edge while editors see real-time changes.
How I build Sanity Portable Text custom components in Next.js
May 16, 2026 · 6 min read
Learn how to build Sanity Portable Text custom components in Next.js — code blocks, callouts, embedded forms, and captioned images with full TypeScript types.