Skip to main content
Avocado Studio doesn’t ship its own content database. Pages live in your CMS (or a JSON file in git, or MDX, or an internal API); the orchestrator holds draft / undo / chat state in SQLite and reads published content from your store via a small adapter. That means you keep your existing CMS, your existing content model, your existing publishing workflow. Avocado adds an AI-native editing surface on top. Switching content stores is a swap of one adapter, not a migration.

The contract

Every adapter implements the same two-method interface from @ai-site-editor/orchestrator-core:
export interface CmsAdapter {
  readonly id: string                                  // "json-file", "sanity", …
  getPages(): Promise<PageDoc[]>                       // seed on cold session
  onPublish?(pages: PageDoc[]): Promise<void>          // optional writeback
}
That’s the whole contract.
  • getPages() is called lazily on the first chat for a fresh session. Its result seeds SQLite so chat turns like “edit the homepage hero” resolve against your real content instead of a 404.
  • onPublish() is called when a client POSTs to /publish on the library-mode handler with { session, siteId }. The orchestrator reads the current draft from SQLite and hands the resulting PageDoc[] to the adapter. If you omit onPublish, /publish becomes a 200 no-op (with written: false in the response) and SQLite still holds the draft.
The adapter is the seed and the sink, never the live working copy. All in-flight edits live in SQLite; that’s what makes the same chat UX work against a static JSON file, a Sanity space, or anything in between.

Bundled adapters

Two adapter implementations ship in @ai-site-editor/orchestrator-core/cms:

jsonFileAdapter

Reads PageDoc[] from a JSON file on disk. Smallest possible adapter — useful when your content is already checked into git, or for prototypes before you pick a real CMS.
import { createOrchestrator } from "@ai-site-editor/site-sdk/server"
import { jsonFileAdapter } from "@ai-site-editor/orchestrator-core/cms"
import path from "node:path"

const handler = createOrchestrator({
  adapter: jsonFileAdapter({
    path: path.join(process.cwd(), "lib", "published-content.json"),
    writeOnPublish: false  // default; flip to true to overwrite the file on publish
  })
})
The file may be either a bare PageDoc[] or an object with a pages key. Each entry is parsed through the lenient PageDoc schema, so partial or extra fields are tolerated.

editorApiAdapter

Fetches PageDoc[] from a site’s /api/editor/pages endpoint. Use this when your site already exposes a page-listing API, or when the orchestrator runs out-of-process from the site.
import { editorApiAdapter } from "@ai-site-editor/orchestrator-core/cms"

const handler = createOrchestrator({
  adapter: editorApiAdapter({
    origin: "https://your-site.com",   // or set AUTO_BOOTSTRAP_SITE_ORIGIN
    path: "/api/editor/pages",         // default
    siteId: "main",                    // optional ?siteId= query
    timeoutMs: 8000,                   // default
    headers: {                         // optional — for protected endpoints
      authorization: `Bearer ${process.env.EDITOR_API_TOKEN}`
    }
  })
})
Pages that fail the lenient PageDoc schema are dropped from the seed and a warning is logged with the candidate index, slug, and first Zod issue path — pass logger: yourLogger if you want those routed to your own log sink.

Wiring it up

Library-mode integration (orchestrator mounted as a Next.js catch-all route inside your site) is the recommended pattern. One file, fully drop-in:
// app/api/avocado/[[...path]]/route.ts
import { createOrchestrator } from "@ai-site-editor/site-sdk/server"
import { jsonFileAdapter } from "@ai-site-editor/orchestrator-core/cms"
import path from "node:path"

export const runtime = "nodejs"
export const dynamic = "force-dynamic"

const handler = createOrchestrator({
  adapter: jsonFileAdapter({
    path: path.join(process.cwd(), "lib", "published-content.json")
  }),
  siteId: "my-site"  // optional; defaults to "library" when adapter is set
})

export const POST = handler
export const GET = handler
export const OPTIONS = handler
That’s it — the first chat turn for a new session calls getPages(), seeds SQLite, and the planner can immediately reason about your pages.

Publishing back

Once adapter.onPublish is defined, your editor (or any client) can publish a session’s draft to the upstream store with one POST:
curl -X POST https://your-site.com/api/avocado/publish \
  -H 'content-type: application/json' \
  -d '{"session":"sess_abc","siteId":"my-site"}'
# → { "ok": true, "written": true, "count": 3 }
The orchestrator reads the current draft for the scoped session out of SQLite and hands the resulting PageDoc[] to adapter.onPublish(pages). If the adapter has no onPublish, the route still returns 200 but with written: false — useful for sites that publish via CI / git commit rather than a runtime writeback.
Session scoping. When adapter is set, sessions auto-scope to siteId (default "library") so they bypass the legacy demo-content seed path. If you see a chat returning demo blocks instead of your content, pass an explicit siteId to force the scope.

Writing a custom adapter

If your content lives somewhere jsonFileAdapter and editorApiAdapter don’t reach, write your own. The contract is small enough to inline:
// lib/my-cms-adapter.ts
import type { CmsAdapter } from "@ai-site-editor/orchestrator-core/cms"
import type { PageDoc } from "@avocadostudio-ai/shared"
import { myCmsClient } from "./my-cms-client"

export function myCmsAdapter(opts: { spaceId: string }): CmsAdapter {
  return {
    id: "my-cms",
    async getPages(): Promise<PageDoc[]> {
      const raw = await myCmsClient.listPages(opts.spaceId)
      return raw.map(mapMyCmsToPageDoc)
    },
    async onPublish(pages: PageDoc[]): Promise<void> {
      await Promise.all(pages.map((p) => myCmsClient.upsert(mapPageDocToMyCms(p))))
    }
  }
}
Then pass the instance into createOrchestrator({ adapter: myCmsAdapter({ spaceId: "..." }) }). Failures inside getPages() are logged but non-fatal — the session simply starts empty. If you build a non-trivial adapter (Storyblok, Hygraph, Payload, Directus, etc.) we’d love to merge it into examples/.

Custom block schemas

If your site renders custom block shapes (e.g. a Hero with carouselImages instead of canonical imageUrl), register your schemas with the global block registry alongside the adapter:
// app/api/avocado/[[...path]]/route.ts
import { createOrchestrator } from "@ai-site-editor/site-sdk/server"
import { jsonFileAdapter } from "@ai-site-editor/orchestrator-core/cms"

// Side-effect import: registers site-specific block schemas with the global
// block registry. MUST be the last import — ESM side effects fire in source
// order, so this runs AFTER orchestrator-core's transitive imports of
// @avocadostudio-ai/shared have registered the canonical schemas. Otherwise
// the canonical registrations will overwrite yours.
import "@/lib/my-blocks"

const handler = createOrchestrator({ adapter: jsonFileAdapter({ path: "..." }) })
Inside lib/my-blocks.ts, call registerBlock("Hero", { schema, meta }) for each type you want to override. See Custom blocks for the full schema shape.
The schema-override import must be the last import in the file. ESM resolves imports up front but fires side effects in source order — if your override runs before the orchestrator’s transitive deps, the canonical schemas will re-register on top of yours.

Example apps

Five working examples live under examples/ in the repo. Each one boots in under two minutes:
ExampleContent storePatternPort
sample-siteLocal JSON fileZero-config starter — uses jsonFileAdapter3002
contentful-siteContentfulHeadless SaaS CMS3003
contentful-marketing-siteContentfulMarketing-site flavor3006
sanity-siteSanityHeadless + embedded Studio3004
strapi-siteStrapi v5Self-hosted open-source CMS3005
Every CMS example (Contentful, Sanity, Strapi) ships with both wirings side-by-side: a library-mode /api/avocado/[[...path]]/route.ts that mounts createOrchestrator({ adapter: ... }) (recommended), plus the legacy split-mode /api/editor/* route for sites that want the orchestrator deployed separately.

Contentful

Free Community plan is enough. The setup script creates the full content model (20 block types + page + siteConfig).
CONTENTFUL_SPACE_ID=<from app.contentful.com Settings General>
CONTENTFUL_DELIVERY_TOKEN=<from API keys Content delivery tokens>
CONTENTFUL_MANAGEMENT_TOKEN=<from API keys Content management tokens>
CONTENTFUL_ENVIRONMENT=master

pnpm --filter contentful-site contentful:setup     # idempotent
pnpm --filter contentful-site dev                  # http://localhost:3003
Full walkthrough: examples/contentful-site/README.md.

Sanity

The example ships with an embedded Sanity Studio at /studio alongside the Avocado editor.
NEXT_PUBLIC_SANITY_PROJECT_ID=<from sanity.io/manage>
NEXT_PUBLIC_SANITY_DATASET=production
SANITY_API_TOKEN=<from API Tokens (Editor permissions)>

pnpm --filter sanity-site sanity:schema-gen        # only after upgrading block fields
pnpm --filter sanity-site dev                      # http://localhost:3004
Add http://localhost:3004 as a CORS origin in Sanity project settings (with credentials allowed). Full walkthrough: examples/sanity-site/README.md.

Strapi

Self-hosted, open-source. The setup script generates Strapi v5 schema files from the Avocado block registry.
npx create-strapi@latest strapi-backend --quickstart --skip-cloud --no-run
STRAPI_PROJECT=/absolute/path/to/strapi-backend pnpm --filter strapi-site strapi:setup
cd /absolute/path/to/strapi-backend && npm run develop    # http://localhost:1337/admin

# In another terminal:
pnpm --filter strapi-site dev                              # http://localhost:3005
Generate an API token from the Strapi admin (Settings → API Tokens) with read+write for page and site-config. Full walkthrough: examples/strapi-site/README.md.

See also