Migrating from Sanity
Step-by-step guide for migrating your Sanity project to CellCMS. CellCMS is designed as a drop-in replacement — your schemas, GROQ queries, and Portable Text content work with minimal changes.
Overview
| What | Sanity | CellCMS |
|---|---|---|
| Schema definitions | defineType, defineField | Same API (compatible) |
| Query language | GROQ | Same (GROQ) |
| Client SDK | @sanity/client | @cellcms/client (API-compatible) |
| Document format | JSON with _id, _type, _rev | Same format |
| Portable Text | @portabletext/react | Same (compatible) |
| Hosting | Sanity Cloud | CellCMS Cloud |
Step 1: Export Your Sanity Data
Use the Sanity CLI to export your dataset:
# Install Sanity CLI if not already installed
npm install -g @sanity/cli
# Export your production dataset
sanity dataset export production production.tar.gz
This creates a .tar.gz archive containing:
data.ndjson— All documents in NDJSON formatimages/— Uploaded imagesfiles/— Uploaded files
Extract the archive:
tar xzf production.tar.gz
Step 2: Set Up CellCMS
Sign up at cellcms.com and create your project. Follow the Getting Started guide for details.
Once your project is ready, verify the API is accessible:
curl https://api.cellcms.com/api/v1/health
Step 3: Import Documents
Import the NDJSON data into CellCMS:
curl -X POST https://api.cellcms.com/api/v1/data/import/production \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/x-ndjson" \
--data-binary @data.ndjson
Response:
{
"total": 1250,
"created": 1230,
"replaced": 0,
"skipped": 20,
"errors": []
}
Notes:
- Sanity system documents (starting with
system.or_.) are automatically skipped - Document IDs, types, and content are preserved exactly
_createdAttimestamps are preserved if present- Use
?replace=trueto overwrite existing documents on re-import
Verify the Import
curl -X POST https://api.cellcms.com/api/v1/data/query/production \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '{"query": "count(*)"}'
Step 4: Migrate Assets
Assets (images and files) need to be uploaded separately. Download them from your Sanity export and upload to CellCMS:
# Upload each image from the extracted export
for file in images/*; do
curl -X POST https://api.cellcms.com/api/v1/assets/production \
-H "Authorization: Bearer YOUR_JWT" \
-F "file=@$file"
done
For large asset libraries, you can script this with a Node.js helper:
import fs from 'fs'
import path from 'path'
const API_URL = 'https://api.cellcms.com'
const TOKEN = 'YOUR_JWT'
async function uploadAssets(dir: string) {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const formData = new FormData()
formData.append('file', new Blob([fs.readFileSync(filePath)]), file)
const response = await fetch(
`${API_URL}/api/v1/assets/production`,
{
method: 'POST',
headers: { Authorization: `Bearer ${TOKEN}` },
body: formData,
}
)
const result = await response.json()
console.log(`Uploaded: ${file} → ${result._id}`)
}
}
uploadAssets('./images')
uploadAssets('./files')
Note: Asset IDs in CellCMS may differ from Sanity. If your documents reference assets by Sanity-generated IDs (e.g.,
image-abc123-1920x1080-jpg), you may need to update these references after upload.
Step 5: Migrate Schemas
CellCMS schema definitions are compatible with Sanity's defineType and defineField:
// Before (Sanity)
import { defineType, defineField } from 'sanity'
// After (CellCMS)
import { defineType, defineField } from '@cellcms/schema'
Most schemas work without changes. Review these potential differences:
Supported Field Types
All standard Sanity field types are supported:
| Type | Status |
|---|---|
string, text, number, boolean | Supported |
date, datetime | Supported |
url, email | Supported |
slug | Supported (with source option) |
image | Supported (with hotspot) |
file | Supported |
reference | Supported (with to) |
array | Supported (with of) |
object | Supported (with nested fields) |
block (Portable Text) | Supported |
geopoint | Supported |
Validation Rules
All standard validation rules are supported:
validation: (Rule) => Rule.required().min(1).max(200)
validation: (Rule) => Rule.regex(/^[a-z]+$/)
validation: (Rule) => Rule.email()
validation: (Rule) => Rule.uri({ scheme: ['http', 'https'] })
validation: (Rule) => Rule.custom((value) => value ? true : 'Required')
Features Not Yet Supported
- Custom input components (Sanity Studio plugins)
- Cross-dataset references
- Content Lake API (Sanity-specific)
- Sanity Vision (GROQ playground) — use curl or the API directly
Step 6: Swap the Client SDK
Replace @sanity/client with @cellcms/client in your frontend:
npm uninstall @sanity/client
npm install @cellcms/client
Update imports:
// Before
import { createClient } from '@sanity/client'
const client = createClient({
projectId: 'your-project-id',
dataset: 'production',
apiVersion: '2024-01-01',
useCdn: true,
token: 'your-sanity-token',
})
// After
import { createClient } from '@cellcms/client'
const client = createClient({
apiUrl: 'https://api.cellcms.com',
project: 'my-project',
dataset: 'production',
token: 'cell_your-api-token',
})
API Differences
| Sanity | CellCMS | Notes |
|---|---|---|
projectId | project | Project slug instead of ID |
apiVersion | — | Not needed |
useCdn | — | Not applicable (managed by CellCMS) |
token (Sanity format) | token (cell_...) | Create via API token management |
Method Compatibility
These methods work identically:
// Queries
client.fetch('*[_type == "post"]')
client.fetch('*[_type == "post" && slug.current == $slug][0]', { slug: 'hello' })
// Mutations
client.create({ _type: 'post', title: 'Hello' })
client.createOrReplace({ _id: 'my-id', _type: 'post', title: 'Hello' })
client.createIfNotExists({ _id: 'my-id', _type: 'post', title: 'Hello' })
client.delete('doc-id')
// Patching
client.patch('doc-id').set({ title: 'Updated' }).commit()
// Listening
client.listen('*[_type == "post"]').subscribe(event => { ... })
Step 7: Update GROQ Queries
Most GROQ queries work without changes. CellCMS supports:
- All comparison operators (
==,!=,>,<,>=,<=) - Logical operators (
&&,||,!) inandmatchoperators- Projections, ordering, slicing
- Reference dereferencing (
->) - Parameters (
$param) - Functions:
count,defined,coalesce,now,length,lower,upper,round,references,sum,avg,min,max,pt::text
GROQ Features Not Yet Supported
score()function (partial — works withmatchfor full-text search)dateTime()functiongeo::namespace functionssanity::namespace functions- Conditional spread (
...condition => {})
If you use any of these, you'll need to find alternative query patterns.
Step 8: Test and Verify
Before switching production traffic:
-
Verify document counts:
curl -X POST https://api.cellcms.com/api/v1/data/query/production \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"query": "count(*[_type == \"post\"])"}' -
Test your key queries: Run every GROQ query your frontend uses and compare results
-
Check assets: Verify images load correctly with transforms
-
Test mutations: Create, update, publish, and delete a test document
-
Test webhooks: If you have build triggers, verify they fire on publish
-
Test real-time: If you use
listen(), verify WebSocket events work
Step 9: DNS Cutover
Once everything is verified:
- Create a read-only API token for your frontend
- Update your frontend's environment variables:
CELLCMS_API_URL=https://api.cellcms.com CELLCMS_TOKEN=cell_your-read-only-token CELLCMS_PROJECT=my-project CELLCMS_DATASET=production - Deploy your frontend
- Monitor for errors in the first few hours
- Cancel your Sanity subscription when confident
Rollback Plan
Keep your Sanity project active until you're fully confident in CellCMS:
- Don't delete your Sanity project immediately
- Keep a copy of your Sanity export
- If issues arise, switch your frontend back to
@sanity/clientand the original environment variables
Related Documentation
- Getting Started — Quick start guide
- Schema Definition — Schema compatibility details
- GROQ Reference — Supported GROQ features
- Client SDK — SDK method reference
- Configuration — Project configuration