Skip to main content
DEMO_MODE=1 turns an 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 DALL-E / Unsplash can’t be triggered
  • Agent and Jira 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.
The gate is server-enforced inside applyOpsAtomically and a Fastify preHandler. Client-trusted flags can’t bypass it. 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.

What demo mode does

SurfaceBehavior in demo mode
/chat, /opsOnly 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.
Image generationShort-circuited in detectImageOps. No external API calls.
Session keysRewritten to demo-<sha1(ip).slice(0,10)> on every request. Clients send anything (e.g. session=dev); middleware swaps it.
/chat*, /opsPer-IP token bucket rate limit (default 20/hr). 429 + retry-after header on exceed.
/status/plannerReturns plannerSource: "demo" so the editor shows its demo badge.
PersistenceSessions are ephemeral — wiped on server restart.

Quick setup

Three Render services (orchestrator + editor + site), each on its own subdomain:

Orchestrator (Render web service)

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
ORCHESTRATOR_DB_FILE=:memory: is auto-set when DEMO_MODE=1 is on; the explicit value is documentation. State is genuinely ephemeral — no disk writes.

Editor (Vercel project, separate from your prod editor)

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 (Vercel project, separate from your prod site)

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
NEXT_PUBLIC_ENABLE_EDITOR=1
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.
# 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_list_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

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