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_propsonHeroblocks) - 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
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:
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
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
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)
VITE_LOCK_SITE_ID=1 hides the site picker; visitors can only edit the one demo site.
Site (separate from your production site)
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.
add_block/remove_block— visitors will spam blocksmove_block— easy to grief the page layout- Anything on
RichText— opens the door to long-form content abuse
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
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
RemoveDEMO_MODE=1 from the orchestrator env and redeploy. There’s no migration — demo state was never persisted.
See also
- Docker deployment — running the orchestrator
- Vercel deployment — splitting editor and site projects
- Chat troubleshooting — diagnosing rejected demo requests