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 astreamIdGET /chat/stream?streamId=…— Subscribe to the stream via SSE; a dropped connection can resubscribePOST /chat/cancel— Stop a running turnPOST /chat— Non-streaming variant (one JSON response)GET /draft/pages— Fetch current draft page statePOST /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 logGET /publish/diff,POST /publish— Review and publish the draft
Editor ↔ site: postMessage
The editor embeds the site in an iframe. They communicate via thesite-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 pagesGET /draft/site-config?session=…&siteId=…— The draft site configuration
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
.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: ThePublishTarget interface is pluggable. Three built-in targets ship in the box:
site-contract— POSTs pages + assets to the remote site’s/api/editor/publishendpoint. Selected whensiteOriginis supplied. The receiving route is the site’s, not the orchestrator’s, and it fails closed twice: 401 when it runs underNODE_ENV=productionwith no publish secret, 409 when the payload would remove every page. Both carry areasonthe 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 rawVERCEL_DEPLOY_HOOK_URLand polls the Vercel API for deployment status.
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.