A hosted CMS studio does not see your app's environment variables
If your hosted Sanity Studio loads with an empty projectId, the app env is probably fine — the studio never received it. Hosted Studio only inlines variables prefixed SANITY_STUDIO_. Mapping NEXT_PUBLIC_SANITY_* at deploy time is a one-line fix that is easy to miss until the CMS is a blank shell.
Two processes, two env contracts
The Next.js site and the Sanity Studio feel like one product. Operationally they are two builds. The site reads NEXT_PUBLIC_SANITY_PROJECT_ID at build and runtime the way every Vercel app does. The hosted Studio at sanity.studio is a separate Vite (or Studio) bundle. Its config is compiled with a different allowlist.
That split is correct. You do not want the Studio to inherit every secret the marketing site can see. It is also easy to forget when the same human maintains both. You set the vars once in Vercel, open the Studio, and assume the CMS is configured. It is not.
What actually ships into the Studio bundle
Sanity's hosted Studio inlines environment variables that start with SANITY_STUDIO_. Everything else is stripped. NEXT_PUBLIC_SANITY_PROJECT_ID and NEXT_PUBLIC_SANITY_DATASET — the names you already use in Next — do not appear in the Studio client unless you also define SANITY_STUDIO_PROJECT_ID and SANITY_STUDIO_DATASET, or you map them in the deploy script.
The failure mode is quiet. The Studio shell renders. Auth may even work. Then projectId is an empty string, the API client has nowhere to go, and you stare at a white content pane wondering whether the dataset is wrong.
How we deploy without duplicating secrets by hand
We keep a single source of truth in the repo's local env and Vercel: NEXT_PUBLIC_SANITY_PROJECT_ID and NEXT_PUBLIC_SANITY_DATASET. The Studio deploy script copies those values into SANITY_STUDIO_* for the duration of the deploy. The Studio config falls back through both names so local embedded /studio and hosted Studio agree.
That is dull plumbing. It is also the difference between a CMS the client can use and a deploy you have to explain on a call. Document the mapping next to the deploy command — future you will not remember why both prefixes exist.
Check the browser, not the server logs
When Studio is broken this way, server logs for the Next app are useless. Open the hosted Studio, inspect the client config or network tab, and look for the project id on the actual requests. Empty string or undefined is the smoking gun.
Once you have seen it once, the checklist is short: Studio prefix present at deploy, dataset matches production, and you are not pointing a production Studio at a personal sandbox project by accident.
Questions
- Why does Next.js see my Sanity project id but hosted Studio does not?
- They are separate builds with different env allowlists. Next inlines NEXT_PUBLIC_*. Hosted Sanity Studio only inlines SANITY_STUDIO_*. Same values, different names, unless you map them at deploy time.
- What is the smallest fix for an empty projectId in Studio?
- Define SANITY_STUDIO_PROJECT_ID and SANITY_STUDIO_DATASET for the Studio deploy (or copy them from your NEXT_PUBLIC_SANITY_* values in the deploy script), then redeploy the Studio.
- Should the Studio bundle include a write token?
- No. Studio users authenticate with their Sanity login. Keep API write tokens on the server or in CI for migrations — not in the client Studio bundle.