Struggling to authenticate draftMode in a Next.js 13 app, seeking advice and finding a solution.
I totally understand the frustration with authenticating draft mode! The good news is that the @sanity/preview-url-secret package is specifically designed to solve this problem. Let me walk you through how to secure it so only users with write access can enable draft mode.
The Solution: Use @sanity/preview-url-secret
This package creates a secure workflow where:
- A user clicks "Open preview" in the Presentation tool
- Sanity generates a short-lived secret (stored in your dataset)
- The secret is validated server-side using a token
- Only if valid, draft mode is enabled
Here's how to set it up for Next.js 13 App Router:
Step 1: Install the package
npm install @sanity/preview-url-secret @sanity/clientStep 2: Configure the Presentation Tool
In your sanity.config.ts:
import { defineConfig } from 'sanity'
import { presentationTool } from 'sanity/presentation'
export default defineConfig({
// ... other config
plugins: [
presentationTool({
previewUrl: {
origin: 'http://localhost:3000', // your Next.js URL
previewMode: {
enable: '/api/draft', // the API route we'll create
},
},
}),
],
})Step 3: Create a Read Token
Create a Sanity API token with Viewer rights (not Editor!). Add it to your .env.local:
SANITY_API_READ_TOKEN=your_token_here
Step 4: Create the Draft Mode API Route
Create app/api/draft/route.ts:
import { client } from '@/sanity/lib/client'
import { validatePreviewUrl } from '@sanity/preview-url-secret'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
const clientWithToken = client.withConfig({
token: process.env.SANITY_API_READ_TOKEN,
})
export async function GET(req: Request) {
const { isValid, redirectTo = '/' } = await validatePreviewUrl(
clientWithToken,
req.url
)
if (!isValid) {
return new Response('Invalid secret', { status: 401 })
}
draftMode().enable()
redirect(redirectTo)
}Step 5: Create a Disable Route (optional but recommended)
Create app/api/disable-draft/route.ts:
import { draftMode } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
export function GET(request: NextRequest) {
draftMode().disable()
return NextResponse.redirect(new URL('/', request.nextUrl.origin))
}How the Security Works
The magic is in validatePreviewUrl(). Here's what it does according to the @sanity/preview-url-secret documentation:
- Checks the secret: When a user clicks "Open preview" in Sanity Studio, a secret document is created in your dataset (type:
sanity.previewUrlSecret) - Validates permissions: The secret can only be created by users with Contributor role or above (those who can create draft documents)
- Short-lived: Secrets expire quickly, preventing unauthorized access
- Server-side validation: The token is never exposed to the client
So effectively, only Sanity users with write access can generate valid preview URLs.
Customizing Permissions (Enterprise)
If you're on an Enterprise plan and want even more control, you can create custom roles that restrict access to preview secrets. The preview-url-secret reference explains how to:
- Grant Viewer roles access to previews
- Restrict who can enable preview sharing
- Control who can see shared preview links
Debugging
If it's still not working, add the debug plugin to see the secrets being created:
import { debugSecrets } from '@sanity/preview-url-secret/sanity-plugin-debug-secrets'
export default defineConfig({
plugins: [
debugSecrets(), // View secrets in Studio
// ... other plugins
],
})The key insight is that @sanity/preview-url-secret ties authentication to Sanity's built-in permission system rather than requiring you to build your own auth layer. Users without write access simply can't generate the secrets needed to enable draft mode.
For more details on the complete setup, check out the Visual Editing with Next.js App Router guide and the Draft Mode documentation.
Hope this helps! Let me know if you run into any issues with the setup.
Sanity β Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.