How to securely use Sanity write token in Next.js app directory?
The issue you're facing is a common security pattern when working with Sanity and Next.js. You're right that you shouldn't expose your write token directly in client-side code. Here's how to solve this properly:
The Problem
When you add the token directly in client code, it works but exposes your write token to anyone who can view your JavaScript bundle - a major security risk. When you use environment variables prefixed with NEXT_PUBLIC_, they get embedded in the client bundle and are publicly accessible.
The Solution: Use Server Actions or API Routes
You need to keep your write token on the server side. With Next.js App Router, you have two good options:
Option 1: Server Actions (Recommended for App Router)
Create a server action that handles the mutation server-side:
app/actions/createReview.ts:
'use server'
import {createClient} from '@sanity/client'
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2024-01-01',
token: process.env.SANITY_WRITE_TOKEN, // NOT prefixed with NEXT_PUBLIC_
useCdn: false,
})
export async function createReview(reviewData: {
name: string
rating: number
comment: string
// ... other fields
}) {
try {
const result = await client.create({
_type: 'review',
...reviewData,
})
return {success: true, data: result}
} catch (error) {
console.error('Failed to create review:', error)
return {success: false, error: 'Failed to create review'}
}
}Your Review component:
'use client'
import {createReview} from '@/app/actions/createReview'
export function ReviewForm() {
async function handleSubmit(formData: FormData) {
const result = await createReview({
name: formData.get('name') as string,
rating: Number(formData.get('rating')),
comment: formData.get('comment') as string,
})
if (result.success) {
// Handle success
}
}
return (
<form action={handleSubmit}>
{/* Your form fields */}
</form>
)
}Option 2: API Route Handler
app/api/reviews/route.ts:
import {createClient} from '@sanity/client'
import {NextResponse} from 'next/server'
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2024-01-01',
token: process.env.SANITY_WRITE_TOKEN, // Server-side only
useCdn: false,
})
export async function POST(request: Request) {
try {
const body = await request.json()
// Add validation here
const result = await client.create({
_type: 'review',
...body,
})
return NextResponse.json({success: true, data: result})
} catch (error) {
return NextResponse.json(
{success: false, error: 'Failed to create review'},
{status: 500}
)
}
}Your client component:
'use client'
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
const response = await fetch('/api/reviews', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(reviewData),
})
const result = await response.json()
}Environment Variables Setup
In your .env.local file:
NEXT_PUBLIC_SANITY_PROJECT_ID=your-project-id
NEXT_PUBLIC_SANITY_DATASET=production
SANITY_WRITE_TOKEN=your-write-token # NO NEXT_PUBLIC_ prefix!Token Permissions
Make sure your token has the right permissions:
- Go to sanity.io/manage or run
npx sanity manage - Navigate to API → Tokens
- Create a token with Editor role (not just Viewer)
- Copy the token to your
.env.localasSANITY_WRITE_TOKEN
The key principle from the API tokens documentation is that write tokens should never be exposed in frontend code. Always use server-side implementations like Server Actions or API routes where the token remains hidden from client code.
Server Actions are generally preferred in App Router as they're simpler to use and automatically handle things like CSRF protection, but both approaches are secure and valid.
Show original thread3 replies
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.