eleven / messenger Public
π€ 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
- Create. The home page's "Try it now" calls the provisioner with
kind=demo. No account, no email, no name. The cloud allocates anentitlement='demo'row in the existingspacesledger (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. - 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.
- 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. - Reap (the default exit). At
created_at + 24hthe 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=falsefor 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:
- 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. - 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. - 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.
- 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 fromLCHAT_CLOUD_MAX_CREATES_PER_DAYand 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. - 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
- PR 1 β ledger + reap.
entitlement='demo'rows, TTL reap rule (unconditional, no archive), the no-replication registry flag. Ships dark behindLCHAT_DEMO_TTL_HOURSunset. - PR 2 β the gates. IP-bucket semaphore + cooldown in home, adaptive PoW difficulty, demo caps in cloud.
- 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). - PR 4 β pool coupling. TTL and live-cap become pressure-tunable once
docs/free-pool.mdPR 3 lands.
Each PR is independently revertible; nothing before PR 3 is user-visible.