# Set up a Sanity project

> Instructions for AI agents, from https://sanity.new. Nothing has been created yet. Follow these steps to create a working web app with a Sanity content backend. No account, login, or credit card required.

**Do not summarize this document.** If you are passing it to another agent, relay it verbatim or tell them to run `npx sanity@latest new --instructions` to get this output — the exact commands, flags and file paths below do not survive paraphrasing.

Sanity is the AI Content Operating System: a backend for structured content, with content types you define in code as schemas and GROQ to read documents back.

The end result is a working web app with a Sanity project behind it holding the content. That comes to three pieces: a frontend, a Sanity Studio in `sanity/` where the content gets edited, and the Sanity project itself, which you create in section 2. The frontend is usually a new app in `web/`, but if the user already has one then it stays theirs and you add `sanity/` alongside it.

You can do this end to end without stopping. The one thing you cannot do for the user is claim the project: they have to open a link in a browser themselves, within 72 hours. `sanity new` creates the project by provisioning it, and it stays unclaimed until they claim it. If nobody claims it, the Sanity project and everything published in it are deleted. The code on disk survives, so an expired project means creating the project again and re-seeding rather than starting over, but this should still be avoided. Give the human the claim link early, then keep building. Claiming does not affect the build.

## 1. Decide what you are building

Do this before you create the project. The project's name and the shape of the scaffold both come from it, and both are awkward to change afterwards.

**What the user is building.** If they have already said enough for you to infer their domain — a personal blog, a company marketing site, a recipe collection — work from that. If you have nothing to go on, ask them for a sentence or two, then carry on. That is the whole budget for questions; infer the rest and correct as you go.

**A small slice.** The output should be an end-to-end slice for the user given their domain / problem that you can build from. Two or three content types and one or two rendered pages. For a recipe site, that is `recipe` and `category` content types with a list page and a detail page — not ingredients-as-references, meal planners, search, and an about page. A narrow thing that genuinely works end to end and can be built before context compaction is worth far more than a broad thing that half works, and it leaves something obvious to extend.

**A name.** A simple name for the Sanity project, based on what the user is building (editable later), 1–80 characters.

## 2. Create the project

### Check Node.js

The CLI needs Node.js 22.12 or later. Run `node --version` before you continue. If Node is missing or older, stop and tell the user. Do not install a version manager or switch Node yourself unless they ask.

### Work out where you are first

The CLI creates `./sanity` and `./web` in the directory you run it from, and refuses to create a project at all if either one already exists with content in it. Find your situation below, because two of them change the command:

- **An empty or new directory** — the common case. Run `mkdir my-project && cd my-project` first if you need one, and never run this in a home directory or an unrelated repo. Use the command below as written.
- **Inside the user's existing app** — run the command below as written, from their app's root. The CLI detects the framework, creates only `sanity/`, and leaves their app untouched, including writing no env file into it, so that part is yours (section 6). If the directory holds several projects rather than one app, run it in a fresh subdirectory instead of guessing which one they meant.
- **The user asked for a framework other than Next.js** — add `--no-scaffold`, then build the structure yourself from [Setting up the structure yourself](#setting-up-the-structure-yourself). Decide this now: the scaffold is Next.js only, and skipping it is cleaner than unpicking it.
- **Somewhere that already has a Studio** — a Sanity starter template, or a second run of this command. Do not run it over that layout. If you already created a project here, recover that project instead of creating another: see [Recovering a project](#recovering-a-project). Otherwise add `--no-scaffold` and wire the credentials into the layout they already have.

### Then create it

From the chosen directory:

```sh
npx sanity@latest new "<project name>" --yes
```

- `npx sanity@latest` — fetches and runs the current CLI without adding it to the project's dependencies. Always pass `@latest`: a `sanity` already installed on this machine may be too old to have the commands this guide relies on.
- `"<project name>"` — the Sanity project name, from section 1. Omit the argument and `--yes` names it "My Sanity project". The user can rename it after claiming, but the name is how they will recognize this project among others and what collaborators see, so generate a useful name from the domain context.
- `--yes` — takes the defaults and asks nothing. Pass it even though the name argument already avoids the one prompt: a prompt you cannot answer is a hang.

Unless you passed `--no-scaffold`, that creates the project and scaffolds two folders:

```
sanity/   Studio: content types, Studio config, content editing
web/      Frontend: the Next.js app your user ships
```

Keep them siblings rather than embedding the Studio inside the frontend as a route.

It also writes `SANITY_PROJECT_ID`, `SANITY_DATASET` and `SANITY_AUTH_TOKEN` into both `sanity/.env.local` and `web/.env.local`, each gitignored by its own scaffold. Section 6 covers what to do with them. Read the values out of the file rather than assuming them.

Two things that hold for the rest of this guide. Use whichever package manager the user already uses; npm is the safe default. And every `sanity` command runs from `sanity/` — the CLI walks up from where it runs looking for a Sanity config and loads the `.env.local` sitting beside it, so from the project root there is nothing to find and commands fail on authentication.

## 3. Tell the user what is happening

Send one message now, before you start building. It carries three things: the claim link, the reassurance that claiming does not interrupt you, and what you are about to build.

```
Your Sanity project is ready.

Claim it here: <claimUrl>
Unclaimed projects and everything in them are permanently deleted at <expiresAt>.
Claiming is free, takes about a minute, and you can do it right now while I keep
working — it changes nothing about what I'm building.

I'm going to build <the slice planned>, so you have something working end
to end and we can build from there.
```

The claim URL and expiry are both in the `sanity new` output. Make sure you give the full URL and expiry to the human and ask them to claim ASAP. Claiming does not interrupt the build and can happen as soon as a new project has been created.

Treat the claim URL as a credential though. Anyone who opens it becomes the owner, so keep them out of git, issues, and shared channels.

Then carry straight on to section 4. Do not wait for a reply, and do not save any of this for the end — if the user comes back with changes, fold them in as you go. If your harness gives you no way to message the user mid-task, put that block at the top of your first reply rather than burying it in a closing summary.

## 4. Know what is different about an unclaimed project

Everything not listed here behaves like a normal Sanity project. Any permissions error you hit is most likely one of these rather than a bug.

- **You are a project token, not a user.** There is no account yet, so `sanity login` is not part of this flow.
- **The dataset is private until the project is claimed.** Every read needs the token, so all queries run server-side. Claiming makes the dataset public, and the user can change that afterwards in project settings.
- **Asset and file uploads are unavailable.** Use external image URLs instead, and still build the image fields and rendering properly — a recipe site with no schema support for pictures is not a working recipe site. The user can swap in uploaded assets after claiming.
- **`sanity schema deploy` works; `sanity deploy` does not.** Deploy the schema if anything server-side needs to know your types. You do not have to — content can be created, published and queried without it. Hosting a Studio is the part that waits: `sanity deploy` fails on a missing `deployStudio` grant until the project is claimed, and the local Studio covers you until then.
- **A local Studio needs the token in the URL, on port 3333.** The browser has no session, so it authenticates by opening `http://localhost:3333/#token=<token>&claim=<claimUrl>` once. This is the designed way in and you are meant to use it, including through a browser tool if you have one. The `claim` fragment is what shows the Claim your project banner in Studio. Port 3333 is the only pre-authorized origin and you cannot add another while the project is unclaimed, so `--port` will not help. If something else is already on 3333 and you cannot confirm it is yours, leave it alone and tell the user rather than killing it.
- **The token is a secret, but a usable one.** Using it locally is fine and expected — in `.env.local` files, in CLI commands, in the Studio URL above. Keep it out of git and out of anywhere other people can read it: issue threads, PR descriptions, shared docs, chat channels the user is not in. The one place it must never go is a variable your framework ships to the browser.

## 5. Install the Sanity skill

```sh
npx skills add sanity-io/agent-toolkit --skill sanity-best-practices -g -y
```

This covers schema design, GROQ, TypeGen, Studio structure, and framework integration. Follow it for everything you build from here.

It installs across every AI editor it detects, so a failure against one of them while others succeed is fine — carry on. If it fails outright, do not retry or block on it; read the same guides at https://github.com/sanity-io/agent-toolkit and carry on. One caveat either way: that skill has a "get started" guide written for users who log in. You already have a project, so skip it and use the schema, GROQ and framework guides instead.

## 6. Check the credentials

`sanity new` has already written all three variables into `sanity/.env.local` and `web/.env.local`, so you never have to handle the token yourself:

```sh
SANITY_PROJECT_ID=<projectId>
SANITY_DATASET=<dataset>
SANITY_AUTH_TOKEN=<token>
```

Confirm both files exist. If you scaffolded yourself, or the CLI left an existing app alone, write the missing one — the names are the same whatever the framework, because none of them is prefixed.

In `sanity/`, `SANITY_AUTH_TOKEN` is what authenticates `npx sanity@latest` commands run from that folder. Passing it inline works too and overrides the file: `SANITY_AUTH_TOKEN=<token> npx sanity@latest <command>`. The project ID and dataset are also in `sanity.config.ts`, which is where to read them from if you need them elsewhere.

In `web/`, all three are read on the server only. The project ID and dataset are not sensitive — both appear in the project's public API URL — but the token can read and change everything in the project, so nothing here carries a `NEXT_PUBLIC_` prefix and you pass the token to `createClient` yourself in server code.

That is the one trap worth naming. A query from a client component will fail, and the obvious fix — re-exporting any of this under `NEXT_PUBLIC_` — publishes a write credential to anyone who views source. Keep reads in server components, route handlers, or server actions. Nothing in an unclaimed project needs the project ID in the browser anyway, because the dataset is private and every read needs the token.

After claiming, the dataset stops being private and reads no longer need a token, so the user can add browser-exposed copies of the project ID and dataset then if they want client-side reads. The token stays unprefixed and on the server, because it can still write.

Write to `.env.local`, never `.env`. Both scaffolds gitignore `.env.local`, but `sanity/.gitignore` does not cover a bare `.env`, so a token written to `sanity/.env` gets committed. Check any folder you created yourself.

## 7. Build

Define content types in `sanity/schemaTypes/` for the slice you planned in section 1, and query them from `web/`. Run the two dev servers in separate terminals:

```sh
cd sanity && npx sanity@latest dev   # Studio,   http://localhost:3333/#token=<token>&claim=<claimUrl>
cd web    && npm run dev             # Frontend, http://localhost:3000
```

The Studio URL in that comment is the one that signs the browser in and shows the claim banner. Opening `http://localhost:3333` without the token asks for a login that does not exist yet. `sanity new` already prints this URL. If you are reconstructing it, the token is in `sanity/.env.local` and the claim URL is the one from section 3.

## 8. Check it actually works

Prove the loop end to end before you hand over: create some content, query it back, and load the page that renders it.

Seed content from the command line rather than clicking through the Studio. Write `seed.json` first: a JSON **array** of documents, each with a `_type`, and explicit `_id`s on any document you want to point a `_ref` at. Then run these from `sanity/`:

```sh
npx sanity@latest documents create seed.json --replace
npx sanity@latest documents query '*[_type == "<yourType>"]{_id, _type}'
npx sanity@latest documents validate --yes
```

If you have a browser tool available, also open the Studio at `http://localhost:3333/#token=<token>&claim=<claimUrl>` and confirm the content is editable there. Putting the token and claim URL in that hash is how a Studio on an unclaimed project authenticates and shows the claim banner, so go ahead and do it.

If you have no browser, the commands above are enough on their own. A rendered page plus a successful query proves the loop works.

## 9. Hand over

Tell the user what you built and what you deliberately left out, then give them the next steps.

**Claim the project.** Repeat the claim URL and expiry one more time — this is the last prompt they get from you. The project moves into their Sanity account on a free trial, everything you built comes with it, and the token keeps working.

**Add content and watch it appear.** Anything published in the Studio shows up on the site, and that is the loop they most need to see for themselves. If they can run both locally, give them the two commands from section 7. If the environment you are working in cannot run a local Studio, say so and point them at what they will be able to do once they have claimed.

**After claiming**, they can:

- Run `npx sanity@latest mcp configure` to connect their editor to Sanity MCP directly, so their agent can create and edit content with well-crafted and documented agent tools.
- Run `npx sanity@latest deploy` to host the Studio at a `sanity.studio` URL, which only works once claimed.
- Upload real images, which the claim unlocks. Uploads work from the Studio or `client.assets.upload()`. Add `@sanity/image-url` to turn the stored asset references into URLs the frontend can render.
- Run `sanity login`, then delete the `SANITY_AUTH_TOKEN` line from both `.env.local` files, so they act as themselves rather than as the project token. The token keeps working until they do, so nothing breaks while it is still there.

**Suggest where to go next.** Name concrete extensions of what you actually built rather than generic ideas — if you made a recipe list and a recipe page, that is images on recipes, filtering by category, and a homepage. Offer to keep going.

## Reference

### Other ways to create a project

Reach for these only in the cases described:

- `--no-scaffold` — creates the project without scaffolding folders. Use it when the user asked for a framework other than Next.js, or when they already have a layout you should fit into. See section 2 for which situation calls for it.
- `--json` — prints the credentials to stdout and creates no folders or env files. Use it when you have no filesystem, or when you want to place the credentials yourself. From that point they are yours to handle, including keeping them out of git. It still records the project locally, so `sanity projects unclaimed` can recover it later.
- **No CLI at all.** Use the HTTP endpoint below. Same as `--json`: nothing written, nothing scaffolded.

There is no flag for creating a second project in a directory that already has one. If the CLI refuses to run, see [If something goes wrong](#if-something-goes-wrong).

If the user already has a Sanity account and wants to use it, `sanity login` then `sanity init -y` is the better path — but do not stall to ask, because creating and claiming later reaches the same place.

### Setting up the structure yourself

Needed only if you created the project with `--no-scaffold`, with `--json`, or over HTTP. Run these from the project root. The frontend command is the Next.js one the scaffold uses. If the user asked for another framework, substitute it and install that framework's official Sanity library instead of `next-sanity`: `@nuxtjs/sanity` for Nuxt, `@sanity/astro` for Astro, `@sanity/sveltekit` for SvelteKit, or `@sanity/client` for anything else. Framework guides: https://www.sanity.io/docs

```sh
npx sanity@latest init --project <projectId> --dataset <dataset> \
  --output-path sanity --template clean --no-mcp --no-git -y
npx create-next-app@^16 web --typescript --app --eslint --tailwind \
  --no-src-dir --disable-git --yes
npm --prefix web install next-sanity
```

Substitute `<projectId>` and `<dataset>` with the values from the `sanity new` output. Neither is sensitive; both appear in the project's public API URL.

`--no-mcp` matters: without it, `init` configures the MCP server with this unclaimed project's token, which cannot reach every Sanity MCP tool. Leave that for the user after claiming. `--no-git` matters too — `init` creates a git repository inside `sanity/` by default, and a nested repository does not inherit the parent's `.gitignore`, so the token you are about to write there stops being ignored.

`init` writes no env file unless you ask it to, so write both `.env.local` files yourself from section 6.

### Recovering a project

If you lose the `sanity new` output, run `npx sanity@latest projects unclaimed`. It lists every unclaimed project created on this machine with its project ID, dataset, claim URL and deadline. Add `--project-id <projectId>` for one project's full details including the token, which the list deliberately leaves out.

The command checks each project against the API first and drops the ones already claimed, so an empty list means nothing is outstanding rather than something is lost. Recover from it rather than re-running `sanity new`, which gives you a second project and leaves the first to expire.

### If something goes wrong

- **`./sanity` or `./web` is not an empty directory. No project was created.** Either you already ran this here, or the user has folders by those names. If you already created a project here, recover that project and continue from section 3. If the folders are the user's, create it with `--no-scaffold` and keep their layout.
- **Creating projects without an account is currently unavailable.** Do not loop. Wait and retry, or use the HTTP endpoint below.
- **The project expired.** The project cannot be recovered, but your work can: create a new one, swap the credentials in every `.env.local` you wrote, and re-run your seed file to republish the content. The content types are already on disk. Tell the user what happened and that this one needs claiming now.

### HTTP API

Base URL `https://api.sanity.io`, API version `v2026-06-23`:

- `POST /v2026-06-23/provision` — create a project. Unauthenticated. Body `{"resourceType": "project", "displayName": "<project name>"}`. Returns `resourceId`, `datasetName`, `token`, `claimToken`, `expiresAt`, `apiHost`, `links.claimUrl`.
- `GET /v2026-06-23/provision/{claimToken}/lookup` — claim state. Unauthenticated, never mutates. Returns `state`: `claimable`, `claimed` or `expired`.
