# 🤖 Demo spaces — instant, no-account, 24-hour trial instances

Design doc, 2026-07-14. Status: **designed, not built.** Companion docs:
`docs/free-pool.md` (the budget that subsidises demos and free spaces) and
`docs/pricing-bands.md` (the pricing that funds the pool). Grounding:
`cloud.go`, `controld.go`, `reaper.go`, `home/pow.go`,
`docs/cloud-provisioner.md`.

## What a demo space is

A real, fully-isolated instance — same binary, same E2EE, same subdomain
scheme (`<sqid>.11ch.at`, riding the wildcard DNS + cert, so creation burns
nothing) — that anyone can create **without an account**, and that the system
**reaps unconditionally 24 hours after creation**. It is the hosted analog of
`lchat try` trials (which mint `try-*.11relay.com` and die with the gateway):
zero-commitment, self-destructing, honest about both.

Why it exists when every account already gets one persistent free space
(`LCHAT_CLOUD_QUOTA=1`): the demo is *try before passkey*. It removes the last
step between "curious" and "typing in a real space" — and its claim path is
the conversion funnel.

A demo is deliberately **strictly worse than a free space**: shorter-lived,
smaller, no backups. That ordering is load-bearing for abuse resistance
(§ Anti-burner): nobody rational farms the worse thing when the better thing
is one passkey away.

## Lifecycle

1. **Create.** The home page's "Try it now" calls the provisioner with
   `kind=demo`. No account, no email, no name. The cloud allocates an
   `entitlement='demo'` row in the existing `spaces` ledger (same table as
   free/paid — one column distinguishes them, mirroring how `'paid'` works
   today) and controld materialises the instance. The creator lands in the
   space with an admin session, exactly like a claimed setup link.
2. **Live.** A persistent in-app banner shows the countdown ("This demo space
   disappears in 19 hours") with a one-tap **Claim** button. The banner is
   inline (never a floating toast) and survives every view.
3. **Claim (the happy exit).** Claim = sign in / create a passkey account on
   home, which converts the row `demo → free` (or straight to paid): the TTL
   is cleared, S3 replication turns on, the space starts counting against the
   account's free quota. If the account's free slot is already taken, the
   claim surface offers the same choices as a quota-blocked create today:
   reclaim your own dead weight, or upgrade. Data is kept intact either way.
4. **Reap (the default exit).** At `created_at + 24h` the reaper deletes the
   instance. Unconditionally: content does not protect a demo (unlike the
   never-set-up rule for real spaces), because the 24h death was disclosed at
   birth and re-disclosed on a banner the whole time.

## Limits (the capability-starvation layer)

Demos are sized so their value to an abuser is lower than the cost of minting
one. Suggested defaults, all env-tunable on the cloud service:

| Limit | Default | Knob |
|---|---|---|
| Lifetime | 24h | `LCHAT_DEMO_TTL_HOURS` (the free pool may shorten it under pressure — see `docs/free-pool.md § amber`) |
| Members | 5 | `LCHAT_DEMO_MAX_MEMBERS` |
| Storage | 25 MB | demo entry in `LCHAT_CLOUD_STORAGE_CAPS` |
| Bots / AI | none | no sidecar is ever provisioned for a demo |
| Backups | none | see below |
| Relay hostname, custom anything | none | demos are `<sqid>.11ch.at` only |

**No backups is a hard property, not a missing feature** (decided 2026-07-14):

- Reaping **skips the trash archive** — a deliberate, documented exception to
  the "deleteInstance never destroys" rule, safe because the death was the
  contract from the first second.
- **No S3 replication, ever.** Implemented structurally (slice 1, PR #874):
  a demo is **never activator-managed at all** — controld forces
  `Hibernate=false` for demo creates even on hibernate-by-default boxes, so
  there is no lease, no litestream replica, and no parked prefix by
  construction. Without this, thousands of 24h demos would pay S3 PUT tails
  and strand orphan prefixes — the exact request-cost class the #836 cadence
  work eliminated. The price is that a demo holds a materialized slot for
  its 24h life, which the live-demo cap bounds; a softer
  "sleep locally without replicating" registry flag remains optional future
  work if demo slot pressure ever warrants it. Claiming a demo converts it
  to an ordinary free space, which hibernates (and replicates) normally.

## Anti-burner gates (the graduated-friction ladder)

The demo path must stay essentially always available to humans while being
uneconomical to farm. Layered, in order of cheapness to evade:

1. **Proof-of-work.** The existing hashcash gate (`home/pow.go`,
   `LCHAT_POW_BITS`) fronts demo creation at a higher difficulty than
   free-space creation, and the difficulty **adapts**: bits scale up with
   trailing-hour demo-creation velocity, so an attack makes minting expensive
   without punishing a quiet Tuesday.
2. **One live demo per IP bucket** — a semaphore, not a counter. Buckets are
   `sha256(ip, rotating_salt)` with short retention (~48h), IPv6 bucketed at
   /56 so single-address rotation buys nothing. Only **home** ever sees the
   IP (it already does, as the TLS terminus for signup); cloud and controld
   see only the opaque bucket hash.
3. **Cooldown ≥ TTL.** After a demo dies (reaped or abandoned), its bucket
   cannot mint another for 24h. This is the single most load-bearing rule:
   it makes serial recreation converge to *one rolling slot* — no better
   than the free tier — so the burner incentive is structurally gone rather
   than merely rate-limited.
4. **Fleet caps, separate from real capacity.** A live-demo cap
   (`LCHAT_DEMO_MAX_LIVE`, suggested 10) and a demo creations/day budget
   (`LCHAT_DEMO_MAX_CREATES_PER_DAY`, suggested 50), both distinct from
   `LCHAT_CLOUD_MAX_CREATES_PER_DAY` and both tunable by the free pool's
   pressure signal — so demo farming can never crowd out real signups, and
   real-signup pressure never kills the try-it path.
5. **Physical backstops** (controld `at_capacity`) remain the only thing that
   can ever hard-refuse a demo; the UI answers "give us a minute" and
   retries, which the 24h churn makes genuinely true.

What we deliberately do **not** do: credit-card gates, phone/email
verification, disposable-domain blocklists, IP reputation services,
fingerprinting. Accounts here are anonymous passkeys by design; the ladder
above (cost imposition + velocity limits + starved capability + separate
pool) carries the weight that identity checks carry elsewhere.

## Where each check lives

| Layer | Sees | Enforces |
|---|---|---|
| home | IP, PoW solution | hashcash difficulty; IP-bucket semaphore + cooldown; relays `kind=demo` |
| cloud | ledger (`entitlement='demo'`), budgets | live cap, daily budget, TTL bookkeeping, claim conversion, storage cap push |
| controld | processes, disk | physical capacity; the no-replication registry flag |
| reaper (cloud) | ledger + controld stats | the unconditional 24h reap, no archive |

This preserves the standing privilege architecture: policy in the sandboxed
cloud service, physics in root controld, IP-adjacent checks in home.

## Phasing

1. **PR 1 — ledger + reap.** `entitlement='demo'` rows, TTL reap rule
   (unconditional, no archive), the no-replication registry flag. Ships dark
   behind `LCHAT_DEMO_TTL_HOURS` unset.
2. **PR 2 — the gates.** IP-bucket semaphore + cooldown in home, adaptive PoW
   difficulty, demo caps in cloud.
3. **PR 3 — the surface.** Home "Try it now", the countdown banner, the claim
   flow (including the quota-blocked variants). *Built:* the home offer + create
   path shipped first; the banner + claim landed next as the in-space band
   ("Keep this space" → home's `#claim=<host>` screen). The banner trusts the
   HOME server's live status (`GET /api/spaces/demo-status`, relaying the
   cloud's per-host read), not the boot-time entitlement env — a claim flips
   the ledger without restarting the instance, so only the live read can hide
   the band. Native apps don't show the band yet (a demo reaching a native app
   is an add-device edge case; deliberate web-first scope).
4. **PR 4 — pool coupling.** TTL and live-cap become pressure-tunable once
   `docs/free-pool.md` PR 3 lands.

Each PR is independently revertible; nothing before PR 3 is user-visible.
