How to securely use Sanity write token in Next.js app directory?

3 replies
Last updated: Sep 29, 2023
hello, I am working on nextjs+ embeded Sanity. I came across a problem. I have a Review component. On button click I want to send a request to sanity to create new review document. And here is the issue. I can only send a request if token is added directly to the client. If I add token to my .env file I get error that I don't have permission. Any ideas what to do in this case? I use Nextjs with app directory
AI Update

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:

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:

  1. Go to sanity.io/manage or run npx sanity manage
  2. Navigate to API → Tokens
  3. Create a token with Editor role (not just Viewer)
  4. Copy the token to your .env.local as SANITY_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 thread
3 replies
Hey
user U
. The Sanity client can freely read information, but you need an API key to perform writes. That being said, you should not expose security-sensitive API keys to the client. The correct route in your case would be to use a Next API route , having your write key as a non-public environment variable (i.e. not starting with
NEXT_PUBLIC_
). You would then call your own API route to achieve the behaviour you want
I see, Thank you for your answer!
Sure thing. Let me know if you need any further help

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.

Was this answer helpful?