Migration

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

WhatSanityCellCMS
Schema definitionsdefineType, defineFieldSame API (compatible)
Query languageGROQSame (GROQ)
Client SDK@sanity/client@cellcms/client (API-compatible)
Document formatJSON with _id, _type, _revSame format
Portable Text@portabletext/reactSame (compatible)
HostingSanity CloudCellCMS 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 format
  • images/ — Uploaded images
  • files/ — 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
  • _createdAt timestamps are preserved if present
  • Use ?replace=true to 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:

TypeStatus
string, text, number, booleanSupported
date, datetimeSupported
url, emailSupported
slugSupported (with source option)
imageSupported (with hotspot)
fileSupported
referenceSupported (with to)
arraySupported (with of)
objectSupported (with nested fields)
block (Portable Text)Supported
geopointSupported

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

SanityCellCMSNotes
projectIdprojectProject slug instead of ID
apiVersionNot needed
useCdnNot 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 (&&, ||, !)
  • in and match operators
  • 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 with match for full-text search)
  • dateTime() function
  • geo:: namespace functions
  • sanity:: 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:

  1. 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\"])"}'
    
  2. Test your key queries: Run every GROQ query your frontend uses and compare results

  3. Check assets: Verify images load correctly with transforms

  4. Test mutations: Create, update, publish, and delete a test document

  5. Test webhooks: If you have build triggers, verify they fire on publish

  6. Test real-time: If you use listen(), verify WebSocket events work


Step 9: DNS Cutover

Once everything is verified:

  1. Create a read-only API token for your frontend
  2. 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
    
  3. Deploy your frontend
  4. Monitor for errors in the first few hours
  5. 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/client and the original environment variables