If your Sanity API token is exposed in a Next.js site, anyone who opens DevTools can read it and use it against your project. This is more common than it should be on inherited codebases, and the fix is quick once you know where to look. This post covers how to confirm the leak, what it can cost you, and the order I'd fix it in.

How a Sanity API token ends up exposed

Three patterns cause almost every case I see on sites built by someone else:

  1. A NEXT_PUBLIC_ prefix on the token. Next.js inlines any variable with that prefix into the browser bundle at build time. A developer adds it to make a client component work, and the token ships to every visitor.
  2. A token in a client component or a shared client file. Even without the prefix, a Sanity client created with a token inside a file imported by a use client component can drag the value into the bundle.
  3. A token committed to git. A .env file checked in by accident, or a token pasted into a config file, sits in history even after you delete it.

A fourth, quieter problem is a token with an Editor role used for plain reads. Nothing is leaked yet, but one slip turns it into a write-access leak.

What it costs to ignore this

The damage depends on the token's role. A read token on a private dataset exposes unpublished drafts, which can mean unreleased pricing, embargoed announcements, or internal notes. A write token lets someone mutate or delete documents, upload assets against your quota, or fill the dataset with junk. Even the cleanup is painful: if you have no backup, restoring content by hand is slow. Unexpected requests can also show up on your bill, which I cover in why Sanity API requests explode on Next.js sites.

So this is worth an afternoon now rather than a weekend later.

Step 1: confirm the exposure

Do these checks before you change anything, so you know what you are rotating and why.

  • Open the live site, view the page source and the loaded JavaScript files, and search for the token value or the variable name.
  • Run a production build locally and search the generated client output for the token.
  • Search the repository and its full git history for the token and for the env variable name.
  • Check the hosting dashboard for variables with a NEXT_PUBLIC_ prefix that contain anything secret.
  • In the Sanity management console, open the API section and list your tokens. Look at each token's role and whether anyone recognises it.

If you find one in the bundle, assume it has been copied.

Step 2: rotate first, then refactor

Do not wait for the code fix. In the Sanity management console, create a replacement token with the lowest role that does the job, then delete the exposed one. Update the environment variable in your hosting dashboard. Anything still using the old token will fail, which is exactly how you discover the hidden consumers: preview routes, webhooks, import scripts, and cron jobs.

For most marketing sites, the live pages need no token at all. Published content on a public dataset can be read without one. A read token is only needed for drafts or for a private dataset.

Step 3: the broken pattern and the fix

This is the pattern I most often find. The token is public and the same client is used everywhere:

// lib/sanity.client.ts (broken) import { createClient } from 'next-sanity' export const client = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, apiVersion: '2025-01-01', useCdn: false, // Inlined into the browser bundle. Anyone can read this. token: process.env.NEXT_PUBLIC_SANITY_API_TOKEN, }) 

Split it into two clients. The public one has no token. The privileged one can only be imported on the server:

// lib/sanity/client.ts (safe for client and server) import { createClient } from 'next-sanity' export const client = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, apiVersion: '2025-01-01', useCdn: true, perspective: 'published', }) // lib/sanity/server-client.ts (server only) import 'server-only' import { client } from './client' export const serverClient = client.withConfig({ token: process.env.SANITY_API_READ_TOKEN, useCdn: false, }) 

The server-only import makes the build fail if a client component ever imports this file, which turns a silent leak into a loud error. The token variable has no NEXT_PUBLIC_ prefix, so it never reaches the browser. Use serverClient only for draft preview and server-side jobs. For the preview flow itself, see Sanity draft mode with Next.js App Router.

Step 4: tighten the rest of the surface

A clean token is only part of the picture. Check these as well:

  • Dataset visibility. If the dataset holds anything you would not publish, consider making it private so every read needs a token. Published marketing content is normally fine as public.
  • CORS origins. Remove wildcard and stale origins. Only enable credentials for origins that need authenticated browser requests, such as your Studio URL.
  • Webhook secrets. If your revalidation endpoint has no signature check, fix that too. I show how in Sanity webhooks and ISR revalidation with HMAC verification.
  • Token roles. One token per purpose, named clearly, with the smallest role. Delete any token nobody can explain.
  • Git history. Rotation makes old leaked values useless, so rewriting history is optional. Do it only if the repo is public and you also want to remove other secrets.

Step 5: verify the fix

Rebuild and redeploy, then repeat the checks from step 1. The old token value and the variable name should be absent from the client bundle and from page source. Load a published page in a private window and confirm it renders. Confirm that draft preview still works for an editor, and that the old token now returns an authorisation error. Finally, check the Sanity management console to confirm the old token is gone.

What to ask a developer

When you hire someone to review this, ask:

  1. Which tokens exist, what role does each have, and what uses it?
  2. Does any token touch client code, and how is that enforced?
  3. Is the dataset public or private, and why?
  4. Which CORS origins are allowed?
  5. Where is the rotation procedure written down?

A good answer is short and specific. If nobody can answer, treat the project as unowned and start from the handover checklist. If the previous developer has gone quiet, taking over an existing Next.js project covers the first week.

Work I do

Checking tokens, roles, CORS, and dataset visibility is part of the Performance & Sanity audit I offer, from $800. You can see what's included on the services and packages page.