> ## 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.

# Demo mode

> Run a locked-down public playground on a shared API key. Server-enforced allow-list, per-IP isolation, rate limiting, no image gen.

This page is for the self-hoster who wants a public playground on their own key. `DEMO_MODE=1` turns a standalone orchestrator deployment into a **public "try before you sign up" playground**. You front the LLM bill on a shared API key, and the orchestrator stops the playground from being abused:

* Only allow-listed operations run (default: `update_props` on `Hero` blocks)
* Each visitor gets an isolated ephemeral session keyed by `sha1(ip)` — no persistence
* Per-IP rate limit (default 20 requests/hour)
* Image generation short-circuited so AI generation / Unsplash can't be triggered
* Agent, Jira and session-listing routes return 403

It's the right answer when you want a marketing landing page where prospects can chat-edit a sample site without authenticating, signing up, or installing anything.

<Warning>
  The gate is **server-enforced**: the operation allow-list runs in the operations engine, and the route gate, rate limit and session rewrite run in the standalone server's request hooks. Client-trusted flags can't bypass it. Library mode has no route gate, so run a demo on the [standalone orchestrator](/operations/docker-deployment). That said, this is a playground gate, not a security boundary — anyone hitting your demo orchestrator is using your LLM budget. Always pair `DEMO_MODE=1` with a rate limit and provider-level spend caps.
</Warning>

## Public demo: every edit, bounded spend

`DEMO_MODE` restricts *what* a visitor can do. If you want visitors to try the full editor on your key — any edit, on their own session — use `PUBLIC_DEMO=1` instead. It is the orchestrator half of the editor's `VITE_PUBLIC_DEMO=1`, which already gives each browser its own session and hides publishing:

| Surface | Behavior with `PUBLIC_DEMO=1` |
| - | - |
| `/chat`, `/chat/start`, `/chat/variations*`, `/image/*`, `/attachment/upload`, `/audio/transcribe`, `/checks/run`, `/preview/screenshot`, `/unsplash/search` | Per-IP limit, default 40 an hour (`PUBLIC_DEMO_RATE_LIMIT_PER_IP_PER_HOUR`). 429 + `retry-after`; the editor explains the limit. |
| `GET`/`DELETE /sessions`, `DELETE /sites`, `GET /telemetry/chat*`, `POST /publish`, `/restore/snapshot`, `/agent/*`, `/sites-agent/*`, `/jira/*`, `/whoami` | 403 — each acts on, or reveals, state beyond the caller's own session. |
| `/ops`, `/draft/*`, `/history/*`, `/suggestions` | Unrestricted. |

It also answers the image-source question up front: with both Unsplash and AI images configured, an ordinary deployment asks "Where should this image come from?" on the first image request, and the public demo uses Unsplash instead. Set `CHAT_IMAGE_SOURCE_DEFAULT` to `unsplash`, `ai` or `ask` to choose for any deployment.

The two flags are independent; `PUBLIC_DEMO` gates no operations. Like the `DEMO_MODE` route gate, the metering lives in the standalone server. The same warning applies: pair it with provider-level spend caps.

## What demo mode does

| Surface | Behavior in demo mode |
| - | - |
| `/chat`, `/ops` | Only allow-listed op types on allow-listed block types succeed. Anything else returns `needs_clarification` with example prompts. |
| `/agent/*`, `/sites-agent/*`, `/jira/*` | 403 — these would bypass the op gate. |
| `/whoami`, `/sessions` | 403 — they would list other visitors' sessions. |
| Image generation | Off while `DEMO_DISABLE_IMAGE_GEN` is on, which is the default. No external image API calls, and `/status/planner` reports `imageGenerate: false`. |
| Session keys | Rewritten to `demo-<sha1(ip).slice(0,10)>` on every request. Clients send anything (e.g. `session=dev`); middleware swaps it. |
| `/chat*`, `/ops` | Per-IP token bucket rate limit (default 20/hr). 429 + `retry-after` header on exceed. |
| `/status/planner` | Returns `plannerSource: "demo"` so the editor shows its demo badge. |
| Persistence | `ORCHESTRATOR_DB_FILE` defaults to `:memory:`, so sessions are wiped on restart. |

## Quick setup

Three services, each on its own subdomain: the orchestrator on a host that runs a long-lived container, and the editor and the site on any static or Next.js host.

### Orchestrator

```bash theme={null}
DEMO_MODE=1
DEMO_ALLOWED_OPS=update_props                  # comma list
DEMO_ALLOWED_BLOCK_TYPES=Hero                  # comma list
DEMO_RATE_LIMIT_PER_IP_PER_HOUR=20
DEMO_DISABLE_IMAGE_GEN=1
ANTHROPIC_API_KEY=<your shared key>
ORCHESTRATOR_CORS_ORIGINS=https://demo-editor.example.com,https://demo-site.example.com
ORCHESTRATOR_DB_FILE=:memory:                  # nothing should persist
```

With `DEMO_MODE=1`, an unset or empty `ORCHESTRATOR_DB_FILE` already means `:memory:`; the explicit value is documentation. An explicit file path would override it. No snapshots are taken of an in-memory store.

### Editor (separate from your production editor)

```bash theme={null}
VITE_DEMO_MODE=1
VITE_ORCHESTRATOR_URL=https://demo-orchestrator.example.com
VITE_SITE_ORIGIN=https://demo-site.example.com
VITE_LOCK_SITE_ID=1
```

`VITE_LOCK_SITE_ID=1` hides the site picker; visitors can only edit the one demo site.

### Site (separate from your production site)

```bash theme={null}
ORCHESTRATOR_URL=https://demo-orchestrator.example.com
NEXT_PUBLIC_EDITOR_ORIGIN=https://demo-editor.example.com
DRAFT_DEFAULT_SESSION=dev
NEXT_PUBLIC_DEFAULT_SITE_ID=avocado-stories
```

The middleware rewrites `body.session` and `body.siteId` on every demo request, so clients don't need to know their demo session key — they just send `session=dev` (or anything) and the orchestrator swaps it for `demo-<hash(ip)>` + `siteId=avocado-stories`.

## Tuning the allow-list

The defaults — `update_props` on `Hero` — are deliberately narrow. They demonstrate the AI editing UX (rewrite a headline, change a tagline, swap CTA copy) without letting visitors restructure pages or evict sample content.

Widen carefully. Each new allowed op type is a new vector for visitors to use your LLM budget.

```bash theme={null}
# Allow editing CTAs too — keeps to single-block prop updates
DEMO_ALLOWED_BLOCK_TYPES=Hero,CTA

# Allow appending items to lists (e.g. add an FAQ entry)
DEMO_ALLOWED_OPS=update_props,add_item
DEMO_ALLOWED_BLOCK_TYPES=Hero,CTA,FAQAccordion
```

What you probably **don't** want to allow in a public playground:

* `add_block` / `remove_block` — visitors will spam blocks
* `move_block` — easy to grief the page layout
* Anything on `RichText` — opens the door to long-form content abuse

If you want a wider playground for a specific audience (design partners, prospects under NDA), gate access at your reverse proxy (basic auth, signed URLs) instead of widening the demo allow-list.

## What demo mode is not

* **Not a multi-tenant SaaS.** Each demo session is keyed by IP, not by user. Two visitors behind the same NAT share a session.
* **Not a security boundary.** It stops casual abuse, not a determined attacker. Use upstream rate limits (Cloudflare, your reverse proxy) and provider spend caps for actual cost control.
* **Not a free trial.** State doesn't persist; visitors can't save their work or come back to it. For a "free trial that converts," look at hosting the full editor behind auth instead.

## Testing locally

```bash theme={null}
cd apps/orchestrator
DEMO_MODE=1 ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY pnpm dev
```

Tests in `apps/orchestrator/src/demo-mode.test.ts` cover the split-allow logic (allow `update_props` but only on Hero, etc.) — useful reference if you're widening the allow-list.

## Reverting

Remove `DEMO_MODE=1` from the orchestrator env and redeploy. There's no migration — demo state was never persisted.

## See also

* [Docker deployment](/operations/docker-deployment) — running the orchestrator
* [Vercel deployment](/operations/vercel-deployment) — splitting editor and site projects
* [Chat troubleshooting](/observability/chat-troubleshooting) — diagnosing rejected demo requests


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