Sanity
Lightweight, dependency-free, in-memory fake of the Sanity content API for testing code that uses the real @sanity/client.
Default port: 4842
Quick start
import { SanityServer } from "./services/sanity/src/server.js";
const server = new SanityServer(4842);
await server.start();
// ... run your app/tests ...
await server.stop();
Point the real client at it (override the API host) or drive the HTTP API directly:
const res = await fetch(
"http://127.0.0.1:4842/v2021-10-21/data/query/production?query=" + encodeURIComponent('*[_type == "post"]'),
{ headers: { Authorization: "Bearer parlel" } },
);
// { ms, query, result: [...] }
Access via MCP / preview URL
When run inside a parlel pool, reachable at its mapped preview URL (e.g.
http://127.0.0.1:4842). MCP clients drive GROQ queries and mutations using
SANITY_TOKEN, SANITY_PROJECT_ID, and SANITY_DATASET. Any non-empty Bearer
token is accepted.
Implemented operations
State is in-memory and ephemeral. All routes require Authorization: Bearer <token>.
Query
GET /v2021-10-21/data/query/:dataset?query=<GROQ>— run a GROQ query. Returns{ ms, query, result }.
Supported (minimal) GROQ:
| Query | Meaning |
|---|---|
* | all documents |
*[_type == "x"] | documents whose _type equals x |
*[_id == "x"] | document(s) by id |
...[0] | trailing [0] picks the first match |
Mutations
POST /v2021-10-21/data/mutate/:dataset— apply mutations[{create},{createIfNotExists},{createOrReplace},{patch},{delete}]. Returns{ transactionId, results:[{id,operation}] }.?returnDocuments=true/?returnIds=trueinclude extra fields.
Documents
GET /v2021-10-21/data/doc/:dataset/:id— fetch a document by id ({ documents: [...] }).
Service & inspection (parlel extensions)
GET /— service metadata.GET /health—{ status: "ok" }.POST /__parlel/reset— reset all in-memory state.OPTIONS *— CORS preflight (204).
Surface coverage
This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.
Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.
| Feature | Status |
|---|---|
| Document mutations: create / createIfNotExists / createOrReplace / patch (set/unset/inc) / delete | ✅ Supported |
GROQ: *, *[_type == "x"], *[_id == "x"], trailing [0] | ✅ Supported |
GET /data/doc/:dataset/:id | ✅ Supported |
| Bearer token validity / dataset ACLs | ✓ By design — Any non-empty credential is accepted — no real secrets needed |
| Full GROQ (projections, joins, ordering, functions, params) | ⟳ Roadmap |
| Assets API, listen (live) endpoint, history | ⟳ Roadmap |
| Real persistence | ✓ By design — In-memory by design — fast, isolated, resets cleanly between tests |
Error shapes
Sanity uses { error: { description, type, statusCode } }:
| Status | When |
|---|---|
401 | missing/invalid Bearer token |
400 | unsupported GROQ query/filter |
404 | unknown route |
Manifest
See services/sanity/manifest.json — name sanity, port 4842, protocol http,
healthcheck /health, env SANITY_TOKEN, SANITY_PROJECT_ID, SANITY_DATASET,
SANITY_BASE_URL.
Configuration — test.env
Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.
SANITY_TOKEN=parlel
SANITY_PROJECT_ID=parlel
SANITY_DATASET=production
SANITY_BASE_URL=http://parlel-bridge:4842
<!-- parlel:testenv:end -->