| 1 | # π€ Conversational onboarding & the admin mini-app β design |
| 2 | |
| 3 | Status: **phases 1β3 implemented** (roles, the setup conversation, the Admin |
| 4 | app v1 β landed with this doc); later phases still proposed. Captured from |
| 5 | design discussion 2026-07-06. |
| 6 | |
| 7 | > **Language note (2026-07-22):** this doc predates the anchor model |
| 8 | > (`accounts-and-recovery.md`) and still says "Save a passkey" / "backup" in |
| 9 | > places. The shipped flows say **"Sign in"** β the member-facing framing is |
| 10 | > signing in with Eleven, never the mechanism. Read the passkey/backup |
| 11 | > wording here as design history, not shipped copy. The "recovery kit" |
| 12 | > (Β§ keys, path 2) is likewise superseded by that doc's paper-kit phase. |
| 13 | Builds on `onboarding-and-home-server.md` (welcome page, home-server vault, |
| 14 | fragility banner β phases 1β3a shipped) and the `/tutorial` mini-app |
| 15 | (`web/static/tutorial.js`), whose client-driven, in-Eleven's-voice pattern |
| 16 | this design generalises. |
| 17 | |
| 18 | > **Follow-on: `conversation-scripts.md`.** With iOS/Mac moving to fully |
| 19 | > native rendering, these conversations become **flows-as-data** interpreted |
| 20 | > by a per-platform engine (web first, Swift when the native surfaces land), |
| 21 | > so one script definition serves every client. The flows and trust |
| 22 | > boundaries described here are unchanged β only where the step logic lives. |
| 23 | |
| 24 | ## Goals |
| 25 | |
| 26 | Two onboarding moments, one shape for both: |
| 27 | |
| 28 | - **Server-admin onboarding.** Standing up a server should land the admin in |
| 29 | the **chat view**, in a conversation with Eleven that walks setup β no |
| 30 | `/admin` console, no forms-first overlay. `/admin` is eventually retired, |
| 31 | replaced by an **admin mini-app** only admins can see. |
| 32 | - **User onboarding.** Joining from an invite link should land the joiner in |
| 33 | the **chat view**, where Eleven runs a welcome script β name, avatar, "save |
| 34 | your profile", then an invitation to take the tour. |
| 35 | |
| 36 | The bar from CLAUDE.md holds: familiar before novel; a command/script invokes a |
| 37 | response *in place*; delight rides on top. Onboarding is a conversation because |
| 38 | that's the most familiar thing in a chat app β not because it's clever. |
| 39 | |
| 40 | ## Non-negotiables |
| 41 | |
| 42 | - **The scripts are deterministic, always.** Client-driven, like `/tutorial` β |
| 43 | never a bot round-trip, never AI. A fresh instance has no sidecar and no API |
| 44 | key, and "enable AI?" is itself one of the setup questions, so AI cannot be a |
| 45 | dependency of asking it. Eleven owns the *voice*; the logic is a script with |
| 46 | tap-chips and captured composer answers. AI Eleven converses *after* setup, |
| 47 | where present. |
| 48 | - **E2E rules unchanged.** Everything the scripts collect lands on existing |
| 49 | trust boundaries: display names encrypted under the instance key; group name, |
| 50 | server photo, and contact email are admin-set **branding/settings** |
| 51 | (plaintext by design, already carved out in CLAUDE.md); no new plaintext |
| 52 | categories are introduced. |
| 53 | - **One voice.** Where a bot sidecar exists, its welcome-DM behaviour stands |
| 54 | down in favour of the client script (see *Joiner flow*): two Elevens must |
| 55 | never greet the same person. |
| 56 | |
| 57 | --- |
| 58 | |
| 59 | ## 1. Prerequisite: admin becomes a role, not just a token |
| 60 | |
| 61 | Today "admin" is only a bearer token (`requireAdmin` checks |
| 62 | `checkAdmin(token)`); roster users carry no admin flag. Everything here β an |
| 63 | admin-only mini-app, "first user is the admin", promoting others, moderators β |
| 64 | hangs off one change: |
| 65 | |
| 66 | - **`users.role`**: `admin | moderator | member`. Moderator is in the enum |
| 67 | from day one, unenforced at first, so it's designed-in rather than bolted on. |
| 68 | - **Role-based admin auth.** `/api/admin/*` accepts *either* the admin token |
| 69 | *or* a member device token whose user has `role=admin`. The admin mini-app |
| 70 | then works with the session the user already has β no token pasting. |
| 71 | - **First claimer is admin.** The first user to claim membership on a fresh |
| 72 | instance gets `role=admin`. Admins can later grant/revoke `admin` (and |
| 73 | `moderator`) on other users via the admin app. |
| 74 | |
| 75 | **The admin token survives as a break-glass API credential** β it's how |
| 76 | `add-instance` provisions, how the runbook recovers access, and it's |
| 77 | auth-not-decryption by design. What retires is the `/admin` *page*, once the |
| 78 | mini-app reaches parity. Losing the token path entirely would leave "admin lost |
| 79 | their only device" with no recovery. |
| 80 | |
| 81 | ### Moderators (the aside, captured) |
| 82 | |
| 83 | Problem: on instances where members can invite (`user_invites`), an invited |
| 84 | member currently has the same social surface as anyone else. Roster-affecting |
| 85 | powers should be tiered: |
| 86 | |
| 87 | | power | member | moderator | admin | |
| 88 | |---|---|---|---| |
| 89 | | invite new people (when `user_invites` on) | β | β | β | |
| 90 | | remove a user from the instance | β | β | β | |
| 91 | | rename another user | β | β | β | |
| 92 | | grant/revoke moderator | β | β | β | |
| 93 | | grant/revoke admin | β | β | β | |
| 94 | | instance settings, branding, bots | β | β | β | |
| 95 | |
| 96 | Enforcement is a later phase (see *Phasing*); the schema and the admin-app UI |
| 97 | should assume it from the start. |
| 98 | |
| 99 | ## 2. The admin mini-app |
| 100 | |
| 101 | A mini-app (per `web/static/apps/README.md`) that replaces the `/admin` |
| 102 | console. Visible only to users with `role=admin` β the gate lives in the app |
| 103 | framework (an app can declare `adminOnly`), not scattered through `shell.js`. |
| 104 | |
| 105 | Surface (parity with today's `admin.js`, reachable in-app instead of on a |
| 106 | separate page): invites, roster (remove/rename, now also roles), group name, |
| 107 | server photo, contact email, `user_invites` toggle, bot config, test push. |
| 108 | Companion-view shape (Chat | Interface) fits naturally: the Interface is the |
| 109 | console; the Chat view is where admin-flavoured bot responses land (e.g. |
| 110 | `/invite` already exists as a command). |
| 111 | |
| 112 | Retirement is two-step: (1) ship the app, `/admin` stays; (2) once parity is |
| 113 | proven in use, `/admin` becomes a redirect/pointer and the page + `admin.js` |
| 114 | are deleted. |
| 115 | |
| 116 | ## 3. Server-admin onboarding (first run) |
| 117 | |
| 118 | ### Entry |
| 119 | |
| 120 | `lchat add-instance <host>` stops printing an `/admin#token=` link and instead |
| 121 | prints a one-time **setup link** β a normal invite marked `grants_admin` |
| 122 | (claiming it creates the membership *and* sets `role=admin`). The printed |
| 123 | admin token / admin link remain available (`lchat admin-link <host>`) as the |
| 124 | break-glass path. |
| 125 | |
| 126 | Landing on the setup link does what invites do today: claim, generate the |
| 127 | keypair, and β already true β the first member bootstraps the **instance key** |
| 128 | client-side (`shell.js`, `generateKey()` when no holders exist). The admin |
| 129 | lands in the chat view with a conversation with Eleven already open. |
| 130 | |
| 131 | ### The setup script |
| 132 | |
| 133 | Client-driven, keyed on: caller has `role=admin` **and** the server-side |
| 134 | `setup_complete` config flag is unset. Steps, each one message + captured |
| 135 | answer or tap-chip, each landing on an *existing* endpoint (now reachable with |
| 136 | role-based auth): |
| 137 | |
| 138 | 1. **Your name** β `postLabel` (encrypted under the instance key, as today). |
| 139 | 2. **Group name** β `POST /api/admin/group`. |
| 140 | 3. **Contact email** β `admin_email` in `POST /api/admin/settings` (exists). |
| 141 | 4. **Server photo** β `POST /api/admin/server-avatar` (plaintext branding, as |
| 142 | designed). Offered as a chip that opens the picker; skippable. |
| 143 | 5. **Enable AI?** β a new instance setting (see Β§6). Presented honestly: on |
| 144 | hosted instances it takes effect; on self-hosted it's a preference the |
| 145 | sidecar respects when one exists. |
| 146 | 6. **Can people invite other people?** β `user_invites` (exists). |
| 147 | 7. **Who is this space for?** (#1374) β the plan question, moved here from the |
| 148 | create flow so creating is never pay-gated. Web-only, and only when home's |
| 149 | public `/api/pricebook` answers with tiers (native wrappers and self-hosted |
| 150 | spaces never see it); a paid answer becomes a "Set up your plan" chip on |
| 151 | the finish line that hands off to home's upgrade picker. The space runs |
| 152 | free until that tap. |
| 153 | 8. **The key ceremony** (next section) β deliberately near-last, so it's the |
| 154 | note the setup ends on. |
| 155 | |
| 156 | On completion the script sets `setup_complete` and hands over: "you can change |
| 157 | any of this later β I've put an **Admin** app in your sidebar." |
| 158 | |
| 159 | Answers are captured the `/tutorial` way β the script asks, and either a chip |
| 160 | tap or the next composer submit is consumed as the answer (deterministic |
| 161 | "extraction"; no parsing cleverness). The conversation itself is local |
| 162 | notices (`buildLocalNotice`), not server messages: nothing needs to persist |
| 163 | except the settings each step writes. |
| 164 | |
| 165 | ### The key ceremony |
| 166 | |
| 167 | One precision the copy must get right: what the admin must not lose is their |
| 168 | **credential bundle** (private key + session), not "the instance key" per se. |
| 169 | The instance key is re-wrappable by any other holder β but on a fresh |
| 170 | one-member server *they are the only holder*, so losing their keys loses the |
| 171 | server transitively. The script says so, sternly, in Eleven's voice: **we |
| 172 | cannot get this back for you, and that is the design working.** |
| 173 | |
| 174 | Offered paths, in order: |
| 175 | |
| 176 | 1. **Primary β "Save a passkey"** (one click). The shipped phase-3a home-server |
| 177 | enrolment popup: credentials into the vault blob, one Face ID. This is the |
| 178 | same mechanism as the fragility banner's CTA; completing it here means the |
| 179 | banner never appears for this user. |
| 180 | 2. **Secondary β download a recovery kit.** A small file (1Password |
| 181 | Emergency-Kit style) for people who won't do passkeys: origin, display |
| 182 | name, the private key, and a re-auth credential. **New work + an open |
| 183 | design question:** session tokens are revocable, so the kit likely wants a |
| 184 | durable server-side credential (a long-lived "recovery token" whose hash |
| 185 | the server stores, exchangeable once for a fresh session β same |
| 186 | hash-only-storage discipline as the admin token). Needs its own short |
| 187 | design pass before building. |
| 188 | 3. **Not offered: screenshots.** Screenshots sync to cloud photo libraries in |
| 189 | plaintext and are casually browsed and shared. Recommending one would |
| 190 | undercut the seriousness the step is trying to convey. (A QR inside the |
| 191 | recovery-kit file is possible later polish for cross-device import.) |
| 192 | |
| 193 | The step is skippable β it's their server β but skipping routes into the |
| 194 | existing fragility banner's double-dismiss discipline rather than silence. |
| 195 | |
| 196 | ## 4. User onboarding (joining from an invite) |
| 197 | |
| 198 | Same dance, assembled almost entirely from shipped parts: |
| 199 | |
| 200 | - **Chat view first, no overlays.** The join flow's name overlay goes away; |
| 201 | claiming an invite lands directly in the chat view with Eleven's welcome |
| 202 | conversation open. |
| 203 | - **Welcome script** (client-driven, keyed on first-open-after-join): |
| 204 | 1. **Name** β `postLabel`. |
| 205 | 2. **Avatar** β existing avatar upload; chip opens the picker; skippable. |
| 206 | 3. **"Save your profile"** β the phase-3a enrolment popup, one click and |
| 207 | redirect. This *is* the post-join nudge from `onboarding-and-home-server.md`, |
| 208 | now delivered in Eleven's voice (one voice: the old backup toast is |
| 209 | already slated for retirement; this replaces the banner's first |
| 210 | appearance too, for joiners who complete it). |
| 211 | 4. **"Take the tour"** β the existing chip into `/tutorial`. |
| 212 | - **Sidecar stand-down.** On bot instances, sidecar-Eleven currently |
| 213 | welcome-DMs new members. The client script owns onboarding; the sidecar's |
| 214 | welcome DM is suppressed (or reduced to creating the DM thread the script |
| 215 | runs in). Two Elevens must not greet the same person. |
| 216 | - **Admin-editable welcome (later).** The script reads an optional |
| 217 | admin-authored welcome blob from config (written via the admin app) and |
| 218 | plays it before/instead of the stock greeting. Cheap hook to leave in the |
| 219 | script's structure now; UI later. |
| 220 | |
| 221 | ## 5. Roles vs identity: who is "the admin" in the script? |
| 222 | |
| 223 | `spaceInfo()` already exposes an administrator identity to the tutorial. With |
| 224 | roles on the roster this stops being config-only: the roster itself knows who |
| 225 | the admins are (useful for "contact an admin" chips, the moderator table |
| 226 | above, and the admin app's role management). `admin_name`/`admin_email` in |
| 227 | settings remain the *outward* contact surface (they're branding-adjacent); |
| 228 | role is the *authorisation* surface. Keep them distinct. |
| 229 | |
| 230 | ## 6. The AI toggle (honest scope) |
| 231 | |
| 232 | "Enable AI on this server?" is a real setting with an operator-shaped |
| 233 | dependency: AI is a **sidecar deployment with an API key**, not something an |
| 234 | instance flag can conjure. Design: |
| 235 | |
| 236 | - A new instance setting (e.g. `ai_enabled`), admin-writable, readable by the |
| 237 | sidecar. |
| 238 | - The sidecar respects it: when off, Eleven runs deterministic-only (the |
| 239 | existing per-bot kind machinery); when on β and a sidecar with a key exists β |
| 240 | conversational AI is active. |
| 241 | - Hosted instances: flipping the toggle works end-to-end (sidecars are |
| 242 | provisioned). Self-hosted without a sidecar: the setting records intent and |
| 243 | the script/admin app says what's missing, pointing at `add-bot` docs. |
| 244 | |
| 245 | This slice gets its own small design pass before building (interaction with |
| 246 | per-bot config, and whether "off" also silences AI for bots other than |
| 247 | Eleven). |
| 248 | |
| 249 | --- |
| 250 | |
| 251 | ## Phasing (each slice independently shippable) |
| 252 | |
| 253 | 1. **Roles** β `users.role`, first-claimer-is-admin, role-based |
| 254 | `requireAdmin` alternative, moderator in the enum (unenforced). Pure |
| 255 | server + schema; no UI change. |
| 256 | 2. **Admin mini-app** β `adminOnly` app gate + the app itself over existing |
| 257 | admin endpoints. `/admin` untouched. |
| 258 | 3. **First-run setup script** β `grants_admin` setup invite from |
| 259 | `add-instance`, the Eleven setup conversation, `setup_complete` flag. |
| 260 | Ends with the passkey step only (recovery kit not yet built). |
| 261 | 4. **Key ceremony completion** β recovery-kit design pass, then the |
| 262 | download path. |
| 263 | 5. **Joiner welcome script** β name/avatar/save-profile/tour, join overlay |
| 264 | removal, sidecar welcome stand-down. |
| 265 | 6. **Moderator enforcement** β the permission table in Β§1 enforced |
| 266 | server-side; role management UI in the admin app. |
| 267 | 7. **AI toggle** β design pass + `ai_enabled` + sidecar respect. |
| 268 | 8. **Retire `/admin`** β after the app proves parity in use. |
| 269 | |
| 270 | ## Open decisions |
| 271 | |
| 272 | - **Recovery kit re-auth credential**: durable recovery token (hash-only |
| 273 | server storage) vs re-using the add-device machinery with a non-expiring |
| 274 | variant. (Β§3, key ceremony β blocks phase 4 only.) |
| 275 | - **Sidecar stand-down mechanism**: suppress the welcome DM entirely, or keep |
| 276 | the sidecar creating the DM thread and have the script play inside it? |
| 277 | - **Does `grants_admin` ride on key-invites or user-invites?** Setup is a |
| 278 | key-invite shape (there's no inviter to wrap thread keys), but confirm |
| 279 | against the invite-redesign fragment format (`#e1.<T>`). |
| 280 | - **Admin-editable welcome**: plain text blob vs a tiny script format |
| 281 | (chips?). Start with text. |
| 282 | - **Moderator table details**: can moderators cancel invites? See other |
| 283 | members' invite links? Decide when enforcing (phase 6). |
| 284 | |