Skip to main content
This page is for the developer or self-hoster who wants the map: which process does what, which package holds it, and how a change travels between them.

System overview

Avocado Studio is a pnpm monorepo with three apps and fourteen packages — twelve published to npm, two private: The orchestrator runs in one of two hosts. In library mode, createOrchestrator() mounts it inside your own Next.js app, usually at /api/avocado, and the site and the orchestrator are one process. The standalone server is a Fastify app on :4200, for Docker and other self-hosted deployments. Both serve the same routes from the same @avocadostudio-ai/orchestrator-core code. It has three parallel front doors: the editor web app for humans, the MCP server for AI assistants in any MCP host, and the Jira integration for ticket-driven workflows. All three go through the same operation pipeline — Zod validation, undo history, version log, demo-mode gating — so anything you can do in the web editor, you can do from an MCP client or a Jira ticket, and the other way round. The Jira integration runs on the standalone server only. See MCP server and Jira integration for setup. That the vocabulary is the same at every door is the point, and it is also the safety boundary: all three doors speak operations, and an operation cannot express a change to your code.

Data flow: from chat to preview

When a user sends a message in the editor, here’s what happens: The validation step is where a malformed plan dies. An operation naming a prop the block does not declare, or a value the schema rejects, never reaches your content — it comes back as a skipped op with a reason, not as a broken page.

Packages

The monorepo includes these packages — the ones under @avocadostudio-ai are published to npm, the @ai-site-editor ones are monorepo-only:

Communication protocols

Editor ↔ orchestrator: HTTP + SSE

The editor communicates with the orchestrator via REST API and Server-Sent Events:
  • POST /chat/start — Start a streamed run, returns a streamId
  • GET /chat/stream?streamId=… — Subscribe to the stream via SSE; a dropped connection can resubscribe
  • POST /chat/cancel — Stop a running turn
  • POST /chat — Non-streaming variant (one JSON response)
  • GET /draft/pages — Fetch current draft page state
  • POST /ops — Apply hand-authored operations (bypassing the planner)
  • POST /history/undo, POST /history/redo, GET /history/log, POST /history/restore — Undo, redo and the version log
  • GET /publish/diff, POST /publish — Review and publish the draft

Editor ↔ site: postMessage

The editor embeds the site in an iframe. They communicate via the site-editor/v1 postMessage protocol:
  • Editor → site: Highlight or scroll to a block, navigate to a page, stream field values while a plan is written (liveDraft), refresh after a change lands (draftUpdated)
  • Site → editor: The block a click selected, route changes, and acknowledgements of applied patches

Site ↔ orchestrator: HTTP

The site fetches draft content from the orchestrator when in draft mode:
  • GET /draft/pages?session=…&slug=… — One draft page by slug. Both parameters are required; an unknown slug is a 404.
  • GET /draft/slugs?session=… — Every slug the session has, which is the route that lists pages
  • GET /draft/site-config?session=…&siteId=… — The draft site configuration
In library mode the site’s own draft reads go to the mounted orchestrator in-process when ORCHESTRATOR_URL is unset or names the mount, so a password-protected mount needs no extra token for them.

Session state

The orchestrator maintains per-session state for each editing session:
  • Draft pages — Current page content with all pending edits
  • Undo and redo — Up to 50 entries per page in each direction
  • Version log — Up to 100 entries, each with a restorable snapshot and who made it
  • Chat history — The last six messages, so a follow-up has context
  • Site config — Name, navigation, theme
Session state is scoped per session ID and site. Multiple users editing different sessions don’t interfere with each other. State is persisted to a SQLite database (.data/orchestrator.db, via better-sqlite3 + WAL). A request’s writes are coalesced for about 30 ms and then written in one transaction, and a clean shutdown flushes any pending write before it exits. Plans held for approval are kept in memory only. A restart drops them, and the person asks again. SQLite is the working copy, not the source of truth. Your CMS / JSON file / custom store is the origin; SQLite holds drafts, undo stacks, and chat history scoped per session. On the first request for a fresh session, the orchestrator calls the configured CmsAdapter.getPages() to seed SQLite. On publish, onPublish(pages) writes back. That separation is what lets the same chat UX work against any upstream store without per-integration handshakes.
The orchestrator runs as a single instance with a local SQLite file. Multi-replica horizontal scaling — which would require moving state to a network-accessible store (Postgres, Turso/libSQL, Redis, etc.) — is on the roadmap but not implemented today.

Publishing pipeline

Publishing promotes draft content to production: The PublishTarget interface is pluggable. Three built-in targets ship in the box:
  • site-contract — POSTs pages + assets to the remote site’s /api/editor/publish endpoint. Selected when siteOrigin is supplied. The receiving route is the site’s, not the orchestrator’s, and it fails closed twice: 401 when it runs under NODE_ENV=production with no publish secret, 409 when the payload would remove every page. Both carry a reason the editor shows.
  • git — Serializes draft pages to JSON, commits, and pushes to a Git branch. A Vercel deploy hook wired to that branch auto-builds.
  • deploy-hook — Calls a raw VERCEL_DEPLOY_HOOK_URL and polls the Vercel API for deployment status.
Register your own via registerPublishTarget() to integrate with any workflow — S3, GitLab Pages, Netlify, a CMS API, a custom CI/CD pipeline. See How it works — Publishing for the full interface.