> ## Documentation Index
> Fetch the complete documentation index at: https://docs.avocadostudio.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Vercel deployment

> What of Avocado Studio can run on Vercel, what cannot keep its state there, and the variables each project needs.

This page is for the self-hoster putting a site that uses Avocado Studio on
Vercel. Vercel runs your site and a static build of the editor well. The part
that needs care is the orchestrator's state: drafts, undo history and the
version log live in a SQLite file, and a serverless function has no disk that
survives.

## What runs where

| Piece | On Vercel? |
| - | - |
| **Your site** (Next.js) | Yes |
| **The editor** | Yes, as a static build of `apps/editor`. Or run `avocado-studio start` on any Node host. |
| **The orchestrator's state** | Not durably. It needs a persistent disk and a single instance. |

```mermaid theme={null}
graph LR
    editor["Editor (static, Vercel)"]
    site["Your site (Vercel)"]
    orch["Orchestrator (host with a persistent disk)"]
    db[("SQLite: drafts, history")]
    cms["CMS / repository"]
    editor -->|"chat, ops, publish"| orch
    editor -->|"frames the preview"| site
    site -->|"draft reads"| orch
    orch --- db
    orch -->|"publish"| cms
```

## Library mode on Vercel

`createOrchestrator()` returns a Web-standard handler, and mounting it as a
Next.js route works on Vercel: requests are answered, the
[access gate](/reference/security) applies, and a page rendered by the same
function reads its drafts in-process. What does not hold is the state behind it.

* A deployed function's filesystem is read-only outside `/tmp`, and `/tmp`
  belongs to one instance and disappears with it.
* Vercel runs as many instances as traffic needs. Each one would keep its own
  drafts.

So an edit can apply, show in the preview, and then be gone on the next
request or after the next cold start. Nothing errors. `GET /status/planner`
reports `persistence.ok: false` with the reason when the store could not open.
See [state and backups](/operations/state-and-backups).

<Warning>
  **For editing in production, run the orchestrator where it has a disk.** Either
  run the [standalone orchestrator](/operations/docker-deployment) and point the
  site at it, or host the Next.js app that mounts library mode on a platform with
  a persistent volume and a single instance. A Vercel deployment of the same app
  can still serve the public site.
</Warning>

When you do mount library mode on Vercel, for example for a preview environment
whose drafts may be thrown away:

* Pass `waitUntil` from `@vercel/functions`, or Next's `after()`, to
  `createOrchestrator({ waitUntil })`. Background check runs after a publish or
  an edit are handed to it, so they are not cut off when the response is sent.
* Chat replies stream over server-sent events, so the function's maximum
  duration bounds the longest chat turn.
* Set `ACCESS_PASSWORD_HASH` or `ORCHESTRATOR_ACCESS_TOKEN`. Under
  `NODE_ENV=production`, library mode with neither refuses every request.

## Site on Vercel, orchestrator elsewhere

The site reads drafts from the orchestrator over HTTP and publishes through its
own `/api/editor/publish`.

| Project | Variable | Notes |
| - | - | - |
| **Site** | `ORCHESTRATOR_URL` | The orchestrator's public URL |
| | `DRAFT_MODE_SECRET` | Must match the value the editor uses. Also set it on the orchestrator. |
| | `PUBLISH_TOKEN` | The publish secret, passed to `createEditorApiHandler` as `publishSecret`. A production build with none answers 401 on `POST /api/editor/publish`. |
| | `NEXT_PUBLIC_EDITOR_ORIGIN` | The editor's origin, for the preview bridge |
| | `EDITOR_CORS_ORIGINS` | The editor's origin, for CORS on `/api/editor/*` |
| | `NEXT_PUBLIC_SITE_URL` | The site's own origin, for canonical and Open Graph URLs |
| | `ORCHESTRATOR_ACCESS_TOKEN` | Needed when the orchestrator gates draft reads, which a library-mode orchestrator in another app does in production |
| **Orchestrator** | `ANTHROPIC_API_KEY` | or `OPENAI_API_KEY` / `GOOGLE_GENAI_API_KEY` |
| | `ORCHESTRATOR_CORS_ORIGINS` | The editor and site origins, comma-separated |
| | `ORCHESTRATOR_PUBLIC_ORIGIN` | Its own public URL |
| | `DRAFT_MODE_SECRET`, `PUBLISH_TOKEN` | The same values as the site's. The editor fetches them after sign-in. |
| | `ACCESS_PASSWORD_HASH` | The editor password |

Write every origin without a trailing slash.

## Publishing from a site on Vercel

Two things about publishing change the moment the site is on Vercel rather than
on your machine.

* **The publish route is closed until you configure it.** `POST /api/editor/publish`
  refuses every request with 401 under `NODE_ENV=production` when no
  `publishSecret` is set, and names `PUBLISH_TOKEN` in the refusal. Set the same
  value on the site project and on the orchestrator.
* **A deployed site cannot rewrite its own content file.** The runtime
  filesystem is read-only outside `/tmp`, so a publish handler that writes a
  JSON file under the project — `createJsonFilePublishHandler`, or
  `jsonFileAdapter({ writeOnPublish: true })` — works in `next dev` and fails
  after deploy. Publishing in production goes to something that persists: a CMS,
  a database, or a git commit that triggers a rebuild.

## The editor on Vercel

The editor is a static Vite app in `apps/editor`, and its `vercel.json` already
rewrites every path to `index.html`.

* Root directory: `apps/editor`
* Build command: `pnpm --filter @ai-site-editor/editor build`
* Output directory: `dist`

| Variable | Notes |
| - | - |
| `VITE_ORCHESTRATOR_URL` | The orchestrator's public URL |
| `VITE_SITE_ORIGIN` | The site the preview frames |
| `VITE_SITE_ID` | The default site id |
| `VITE_LOCK_SITE_ID` | `1` hides the site picker |

<Warning>
  **`VITE_*` values are compiled into JavaScript that anyone can download.** Do
  not set `VITE_SITE_DRAFT_SECRET` or `VITE_PUBLISH_TOKEN` on a public editor. Put
  `DRAFT_MODE_SECRET`, `PUBLISH_TOKEN` and `ACCESS_PASSWORD_HASH` on the
  orchestrator instead. After sign-in, the editor fetches the secret and the
  token from `GET /editor/credentials`. That route answers 403 in production until
  a credential is configured.
</Warning>

The alternative to a build is the prebuilt editor in `@avocadostudio-ai/cli`.
Run `avocado-studio start --host 0.0.0.0` on any Node 22 host. On a
non-loopback bind it leaves the secrets out of the page for the same reason. See
[CLI and packages](/reference/cli).

## This repository's demo site

The steps above are for your own site. Deploying `apps/site` from this
repository has two details of its own:

* Root directory `apps/site`. Its build runs `scripts/sync-published-content.mjs`
  and then `next build`.
* With `ORCHESTRATOR_URL` unset, the build keeps the committed
  `lib/published-content.json` and the site needs no orchestrator at runtime.
  With it set, `SITE_PUBLISH_SITE_ID` is **required** or the build fails, and
  `SITE_PUBLISH_SESSION` defaults to `dev`.

`NEXT_PUBLIC_ENABLE_EDITOR=1` only exposes the `/catalogue` route. It has no
effect on draft mode or on the preview bridge.

To keep editing away from production, give the editor a separate staging site
to frame, and point the git publish target at a staging branch with
`PUBLISH_GIT_BRANCH`.

## Orchestrator from source

The standalone orchestrator can also run from source on any Node host with a
persistent disk, without the Docker image. It runs TypeScript directly with
`tsx`, which is a production dependency, and its build step is a typecheck.

* Node 22 or later
* Root directory: `apps/orchestrator`
* Build: `pnpm install --frozen-lockfile && pnpm --filter @ai-site-editor/orchestrator build`
* Start: `pnpm --filter @ai-site-editor/orchestrator start`, which runs `tsx src/index.ts`
* Mount the persistent disk where `ORCHESTRATOR_DB_FILE` points
* One instance only

## Production troubleshooting

**The preview shows the published page, never the draft.** Check these in order:

1. The site's `DRAFT_MODE_SECRET` differs from the one the editor uses. The
   preview degrades to published content rather than failing.
2. The editor could not fetch its credentials. A public editor needs
   `ACCESS_PASSWORD_HASH`, `DRAFT_MODE_SECRET` and `PUBLISH_TOKEN` on the
   orchestrator, or `GET /editor/credentials` answers 403.
3. The site reads drafts from a different orchestrator, or with no access
   token where one is required. A 401 on the draft read is logged on the site.

**Clicking in the preview selects nothing.** Click-to-select is behind the
selection-mode toggle, which is off on first load. If it is on and still
nothing happens, check that the editor's origin is in
`NEXT_PUBLIC_EDITOR_ORIGIN` or `AVOCADO_EDITOR_ORIGINS`. The bridge only talks
to an origin on that list.

**The editor says it cannot reach the orchestrator, but `curl` gets a 200.**
That is CORS. `ORCHESTRATOR_CORS_ORIGINS` on a standalone orchestrator, or
`corsOrigins` in library mode, must include the editor's origin.

**The editor has no pages to edit.** The editor seeds the orchestrator from the
site's `/api/editor/pages`. Without that route, the orchestrator has no draft
pages for the site.

**Edits vanish between requests or after a deploy.** The orchestrator's state is
not on a persistent disk, or more than one instance is running. See
[library mode on Vercel](#library-mode-on-vercel).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.