| 1 | # π€ Demo spaces β instant, no-account, 24-hour trial instances |
| 2 | |
| 3 | Design doc, 2026-07-14. Status: **designed, not built.** Companion docs: |
| 4 | `docs/free-pool.md` (the budget that subsidises demos and free spaces) and |
| 5 | `docs/pricing-bands.md` (the pricing that funds the pool). Grounding: |
| 6 | `cloud.go`, `controld.go`, `reaper.go`, `home/pow.go`, |
| 7 | `docs/cloud-provisioner.md`. |
| 8 | |
| 9 | ## What a demo space is |
| 10 | |
| 11 | A real, fully-isolated instance β same binary, same E2EE, same subdomain |
| 12 | scheme (`<sqid>.11ch.at`, riding the wildcard DNS + cert, so creation burns |
| 13 | nothing) β that anyone can create **without an account**, and that the system |
| 14 | **reaps unconditionally 24 hours after creation**. It is the hosted analog of |
| 15 | `lchat try` trials (which mint `try-*.11relay.com` and die with the gateway): |
| 16 | zero-commitment, self-destructing, honest about both. |
| 17 | |
| 18 | Why it exists when every account already gets one persistent free space |
| 19 | (`LCHAT_CLOUD_QUOTA=1`): the demo is *try before passkey*. It removes the last |
| 20 | step between "curious" and "typing in a real space" β and its claim path is |
| 21 | the conversion funnel. |
| 22 | |
| 23 | A demo is deliberately **strictly worse than a free space**: shorter-lived, |
| 24 | smaller, no backups. That ordering is load-bearing for abuse resistance |
| 25 | (Β§ Anti-burner): nobody rational farms the worse thing when the better thing |
| 26 | is one passkey away. |
| 27 | |
| 28 | ## Lifecycle |
| 29 | |
| 30 | 1. **Create.** The home page's "Try it now" calls the provisioner with |
| 31 | `kind=demo`. No account, no email, no name. The cloud allocates an |
| 32 | `entitlement='demo'` row in the existing `spaces` ledger (same table as |
| 33 | free/paid β one column distinguishes them, mirroring how `'paid'` works |
| 34 | today) and controld materialises the instance. The creator lands in the |
| 35 | space with an admin session, exactly like a claimed setup link. |
| 36 | 2. **Live.** A persistent in-app banner shows the countdown ("This demo space |
| 37 | disappears in 19 hours") with a one-tap **Claim** button. The banner is |
| 38 | inline (never a floating toast) and survives every view. |
| 39 | 3. **Claim (the happy exit).** Claim = sign in / create a passkey account on |
| 40 | home, which converts the row `demo β free` (or straight to paid): the TTL |
| 41 | is cleared, S3 replication turns on, the space starts counting against the |
| 42 | account's free quota. If the account's free slot is already taken, the |
| 43 | claim surface offers the same choices as a quota-blocked create today: |
| 44 | reclaim your own dead weight, or upgrade. Data is kept intact either way. |
| 45 | 4. **Reap (the default exit).** At `created_at + 24h` the reaper deletes the |
| 46 | instance. Unconditionally: content does not protect a demo (unlike the |
| 47 | never-set-up rule for real spaces), because the 24h death was disclosed at |
| 48 | birth and re-disclosed on a banner the whole time. |
| 49 | |
| 50 | ## Limits (the capability-starvation layer) |
| 51 | |
| 52 | Demos are sized so their value to an abuser is lower than the cost of minting |
| 53 | one. Suggested defaults, all env-tunable on the cloud service: |
| 54 | |
| 55 | | Limit | Default | Knob | |
| 56 | |---|---|---| |
| 57 | | Lifetime | 24h | `LCHAT_DEMO_TTL_HOURS` (the free pool may shorten it under pressure β see `docs/free-pool.md Β§ amber`) | |
| 58 | | Members | 5 | `LCHAT_DEMO_MAX_MEMBERS` | |
| 59 | | Storage | 25 MB | demo entry in `LCHAT_CLOUD_STORAGE_CAPS` | |
| 60 | | Bots / AI | none | no sidecar is ever provisioned for a demo | |
| 61 | | Backups | none | see below | |
| 62 | | Relay hostname, custom anything | none | demos are `<sqid>.11ch.at` only | |
| 63 | |
| 64 | **No backups is a hard property, not a missing feature** (decided 2026-07-14): |
| 65 | |
| 66 | - Reaping **skips the trash archive** β a deliberate, documented exception to |
| 67 | the "deleteInstance never destroys" rule, safe because the death was the |
| 68 | contract from the first second. |
| 69 | - **No S3 replication, ever.** Implemented structurally (slice 1, PR #874): |
| 70 | a demo is **never activator-managed at all** β controld forces |
| 71 | `Hibernate=false` for demo creates even on hibernate-by-default boxes, so |
| 72 | there is no lease, no litestream replica, and no parked prefix by |
| 73 | construction. Without this, thousands of 24h demos would pay S3 PUT tails |
| 74 | and strand orphan prefixes β the exact request-cost class the #836 cadence |
| 75 | work eliminated. The price is that a demo holds a materialized slot for |
| 76 | its 24h life, which the live-demo cap bounds; a softer |
| 77 | "sleep locally without replicating" registry flag remains optional future |
| 78 | work if demo slot pressure ever warrants it. Claiming a demo converts it |
| 79 | to an ordinary free space, which hibernates (and replicates) normally. |
| 80 | |
| 81 | ## Anti-burner gates (the graduated-friction ladder) |
| 82 | |
| 83 | The demo path must stay essentially always available to humans while being |
| 84 | uneconomical to farm. Layered, in order of cheapness to evade: |
| 85 | |
| 86 | 1. **Proof-of-work.** The existing hashcash gate (`home/pow.go`, |
| 87 | `LCHAT_POW_BITS`) fronts demo creation at a higher difficulty than |
| 88 | free-space creation, and the difficulty **adapts**: bits scale up with |
| 89 | trailing-hour demo-creation velocity, so an attack makes minting expensive |
| 90 | without punishing a quiet Tuesday. |
| 91 | 2. **One live demo per IP bucket** β a semaphore, not a counter. Buckets are |
| 92 | `sha256(ip, rotating_salt)` with short retention (~48h), IPv6 bucketed at |
| 93 | /56 so single-address rotation buys nothing. Only **home** ever sees the |
| 94 | IP (it already does, as the TLS terminus for signup); cloud and controld |
| 95 | see only the opaque bucket hash. |
| 96 | 3. **Cooldown β₯ TTL.** After a demo dies (reaped or abandoned), its bucket |
| 97 | cannot mint another for 24h. This is the single most load-bearing rule: |
| 98 | it makes serial recreation converge to *one rolling slot* β no better |
| 99 | than the free tier β so the burner incentive is structurally gone rather |
| 100 | than merely rate-limited. |
| 101 | 4. **Fleet caps, separate from real capacity.** A live-demo cap |
| 102 | (`LCHAT_DEMO_MAX_LIVE`, suggested 10) and a demo creations/day budget |
| 103 | (`LCHAT_DEMO_MAX_CREATES_PER_DAY`, suggested 50), both distinct from |
| 104 | `LCHAT_CLOUD_MAX_CREATES_PER_DAY` and both tunable by the free pool's |
| 105 | pressure signal β so demo farming can never crowd out real signups, and |
| 106 | real-signup pressure never kills the try-it path. |
| 107 | 5. **Physical backstops** (controld `at_capacity`) remain the only thing that |
| 108 | can ever hard-refuse a demo; the UI answers "give us a minute" and |
| 109 | retries, which the 24h churn makes genuinely true. |
| 110 | |
| 111 | What we deliberately do **not** do: credit-card gates, phone/email |
| 112 | verification, disposable-domain blocklists, IP reputation services, |
| 113 | fingerprinting. Accounts here are anonymous passkeys by design; the ladder |
| 114 | above (cost imposition + velocity limits + starved capability + separate |
| 115 | pool) carries the weight that identity checks carry elsewhere. |
| 116 | |
| 117 | ## Where each check lives |
| 118 | |
| 119 | | Layer | Sees | Enforces | |
| 120 | |---|---|---| |
| 121 | | home | IP, PoW solution | hashcash difficulty; IP-bucket semaphore + cooldown; relays `kind=demo` | |
| 122 | | cloud | ledger (`entitlement='demo'`), budgets | live cap, daily budget, TTL bookkeeping, claim conversion, storage cap push | |
| 123 | | controld | processes, disk | physical capacity; the no-replication registry flag | |
| 124 | | reaper (cloud) | ledger + controld stats | the unconditional 24h reap, no archive | |
| 125 | |
| 126 | This preserves the standing privilege architecture: policy in the sandboxed |
| 127 | cloud service, physics in root controld, IP-adjacent checks in home. |
| 128 | |
| 129 | ## Phasing |
| 130 | |
| 131 | 1. **PR 1 β ledger + reap.** `entitlement='demo'` rows, TTL reap rule |
| 132 | (unconditional, no archive), the no-replication registry flag. Ships dark |
| 133 | behind `LCHAT_DEMO_TTL_HOURS` unset. |
| 134 | 2. **PR 2 β the gates.** IP-bucket semaphore + cooldown in home, adaptive PoW |
| 135 | difficulty, demo caps in cloud. |
| 136 | 3. **PR 3 β the surface.** Home "Try it now", the countdown banner, the claim |
| 137 | flow (including the quota-blocked variants). *Built:* the home offer + create |
| 138 | path shipped first; the banner + claim landed next as the in-space band |
| 139 | ("Keep this space" β home's `#claim=<host>` screen). The banner trusts the |
| 140 | HOME server's live status (`GET /api/spaces/demo-status`, relaying the |
| 141 | cloud's per-host read), not the boot-time entitlement env β a claim flips |
| 142 | the ledger without restarting the instance, so only the live read can hide |
| 143 | the band. Native apps don't show the band yet (a demo reaching a native app |
| 144 | is an add-device edge case; deliberate web-first scope). |
| 145 | 4. **PR 4 β pool coupling.** TTL and live-cap become pressure-tunable once |
| 146 | `docs/free-pool.md` PR 3 lands. |
| 147 | |
| 148 | Each PR is independently revertible; nothing before PR 3 is user-visible. |
| 149 | |