# Public Read-only Demo Mode

> Applies to: ChengOS v0.2.0+ ｜ Last verified: 2026-09-25 ｜ Source: `crates/cheng-api/src/middleware/demo_mode.rs`, `crates/cheng-api/src/rest/handlers/auth/mod.rs`, `deploy/README.md`, `deploy/.env.example`

Demo mode turns a ChengOS deployment into a public, **read-only** showcase. Visitors do not sign
in: the UI enters the app as one fixed demo account, and they can browse workflows, canvases,
conversations, and documents — but nothing they do can change anything, run anything, or read a
secret.

Run a demo as its **own deployment with its own database**, seeded with the content you want to
show. Never switch a production instance into demo mode.

## How it works

Two settings in `deploy/.env` drive it:

| Variable | Meaning |
|---|---|
| `CHENG_DEMO_MODE` | `true` enables the demo policy (also accepts `1`, `yes`, `on`). Default `false`. |
| `CHENG_DEMO_USER_EMAIL` | The ordinary account visitors are signed in as. |

With demo mode on, the UI calls `POST /api/v1/auth/demo`. That endpoint takes no credentials
and returns a normal session for the demo account. It refuses to run when:

| Response | Reason |
|---|---|
| `404 DEMO_MODE_DISABLED` | Demo mode is off — the endpoint does not exist on a normal deployment |
| `503 DEMO_USER_UNAVAILABLE`, `reason: unset` | `CHENG_DEMO_USER_EMAIL` is not set |
| `503 DEMO_USER_UNAVAILABLE`, `reason: not_found` | No active account has that email |
| `503 DEMO_USER_UNAVAILABLE`, `reason: admin` | The account is a platform administrator — **never** handed out |

Signing in writes nothing: no last-login stamp, no workspace repair. The demo database is
prepared ahead of time.

## What visitors can and cannot do

The policy is an **allowlist applied to the whole API router**, so a route added in a future
release is denied until someone explicitly reviews it. It fails closed.

**Allowed**

- `GET` / `HEAD` reads under `/api/v1/` — the whole app renders.
- `GET /health`, `GET /ready`.
- `POST /api/v1/auth/demo`, `/auth/refresh`, `/auth/logout`, plus `GET /auth/me`,
  `/auth/validate`, `/auth/bootstrap-status`.
- `POST /api/v1/nodes/schemas:resolve` — a bounded read that happens to use `POST`.
- `GET /api/v1/llm/providers` and `/llm/models`, which in demo mode return only the catalog
  from `providers.toml` and never touch stored credentials or probe remote APIs.
- A workspace itself and its knowledge bases.

**Refused with `403 DEMO_MODE_RESTRICTED`**

- Every write: any `POST`, `PUT`, `PATCH`, or `DELETE` not listed above — including login,
  registration, and password changes.
- Every execution transport, and every WebSocket.
- Reads under these prefixes: `/admin`, `/account`, `/auth` (except the session endpoints
  above), `/channel`, `/credentials`, `/deployment-settings`, `/internal`, `/llm` (except the
  catalog), `/mcp`, `/settings`, `/setup`, `/skill-registry`, `/translate`, `/webhook`, `/ws`.
- A workspace's members, channels, channel keys, settings, and storage providers.

`403 DEMO_MODE_RESTRICTED` means "this deployment is a demo", not a permission problem with the
account.

Demo mode also stops the server from changing itself: `RUN_MIGRATIONS` is ignored, and the
scheduler, MCP recovery, channel polling and streams, OAuth token refresh, code indexing, file
watchers, and cleanup jobs do not start. The API logs a warning at startup saying so.

## Setting one up

Prepare the content with demo mode **off**, on a trusted network, then switch:

```bash
# 1. Normal mode: set up the demo content.
#    deploy/.env: CHENG_DEMO_MODE=false
./chengos.sh start
#    In the UI, over a trusted network: create the demo account (an ordinary,
#    non-admin user) and its default workspace, then build what you want to show.
./chengos.sh stop

# 2. Demo mode.
#    deploy/.env: CHENG_DEMO_MODE=true
#                 CHENG_DEMO_USER_EMAIL=demo@example.com
./chengos.sh start
```

Then verify from outside:

```bash
curl -s -X POST https://demo.example.com/api/v1/auth/demo | head -c 200   # a token
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
  https://demo.example.com/api/v1/auth/login                                # 403
```

## Maintenance

Anything that changes the demo — a release upgrade that migrates the database, new content, a
reset — happens with demo mode **off**:

1. Take the instance off the public internet.
2. Set `CHENG_DEMO_MODE=false` and restart.
3. Upgrade, migrate, or edit the content.
4. Set `CHENG_DEMO_MODE=true`, restart, and put it back online.

## Network exposure

Demo mode protects the API's behaviour, not its ports. Expose only your reverse proxy or tunnel,
exactly as for any public deployment — see
[Hardening a Public Deployment](/docs/deploy-hardening). The API stays on loopback, and the
database, Valkey, and Qdrant ports are never public.

## Next steps

- [Hardening a Public Deployment](/docs/deploy-hardening) — the network side of a public demo.
- [Authentication](/docs/api-authentication) — the session the demo login issues.
- [Environment Variable Reference](/docs/config-env-reference) — `CHENG_DEMO_MODE` and friends.
