eleven / messenger Public
π€ Conversational onboarding & the admin mini-app β design
Status: phases 1β3 implemented (roles, the setup conversation, the Admin app v1 β landed with this doc); later phases still proposed. Captured from design discussion 2026-07-06.
> Language note (2026-07-22): this doc predates the anchor model > (accounts-and-recovery.md) and still says "Save a passkey" / "backup" in > places. The shipped flows say "Sign in" β the member-facing framing is > signing in with Eleven, never the mechanism. Read the passkey/backup > wording here as design history, not shipped copy. The "recovery kit" > (Β§ keys, path 2) is likewise superseded by that doc's paper-kit phase. Builds on onboarding-and-home-server.md (welcome page, home-server vault, fragility banner β phases 1β3a shipped) and the /tutorial mini-app (web/static/tutorial.js), whose client-driven, in-Eleven's-voice pattern this design generalises.
> Follow-on: conversation-scripts.md. With iOS/Mac moving to fully > native rendering, these conversations become flows-as-data interpreted > by a per-platform engine (web first, Swift when the native surfaces land), > so one script definition serves every client. The flows and trust > boundaries described here are unchanged β only where the step logic lives.
Goals
Two onboarding moments, one shape for both:
- Server-admin onboarding. Standing up a server should land the admin in the chat view, in a conversation with Eleven that walks setup β no
/adminconsole, no forms-first overlay./adminis eventually retired, replaced by an admin mini-app only admins can see. - User onboarding. Joining from an invite link should land the joiner in the chat view, where Eleven runs a welcome script β name, avatar, "save your profile", then an invitation to take the tour.
The bar from CLAUDE.md holds: familiar before novel; a command/script invokes a response in place; delight rides on top. Onboarding is a conversation because that's the most familiar thing in a chat app β not because it's clever.
Non-negotiables
- The scripts are deterministic, always. Client-driven, like
/tutorialβ never a bot round-trip, never AI. A fresh instance has no sidecar and no API key, and "enable AI?" is itself one of the setup questions, so AI cannot be a dependency of asking it. Eleven owns the voice; the logic is a script with tap-chips and captured composer answers. AI Eleven converses after setup, where present. - E2E rules unchanged. Everything the scripts collect lands on existing trust boundaries: display names encrypted under the instance key; group name, server photo, and contact email are admin-set branding/settings (plaintext by design, already carved out in CLAUDE.md); no new plaintext categories are introduced.
- One voice. Where a bot sidecar exists, its welcome-DM behaviour stands down in favour of the client script (see Joiner flow): two Elevens must never greet the same person.
---
1. Prerequisite: admin becomes a role, not just a token
Today "admin" is only a bearer token (requireAdmin checks checkAdmin(token)); roster users carry no admin flag. Everything here β an admin-only mini-app, "first user is the admin", promoting others, moderators β hangs off one change:
users.role:admin | moderator | member. Moderator is in the enum from day one, unenforced at first, so it's designed-in rather than bolted on.- Role-based admin auth.
/api/admin/*accepts either the admin token or a member device token whose user hasrole=admin. The admin mini-app then works with the session the user already has β no token pasting. - First claimer is admin. The first user to claim membership on a fresh instance gets
role=admin. Admins can later grant/revokeadmin(andmoderator) on other users via the admin app.
The admin token survives as a break-glass API credential β it's how add-instance provisions, how the runbook recovers access, and it's auth-not-decryption by design. What retires is the /admin page, once the mini-app reaches parity. Losing the token path entirely would leave "admin lost their only device" with no recovery.
Moderators (the aside, captured)
Problem: on instances where members can invite (user_invites), an invited member currently has the same social surface as anyone else. Roster-affecting powers should be tiered:
| power | member | moderator | admin | |---|---|---|---| | invite new people (when user_invites on) | β | β | β | | remove a user from the instance | β | β | β | | rename another user | β | β | β | | grant/revoke moderator | β | β | β | | grant/revoke admin | β | β | β | | instance settings, branding, bots | β | β | β |
Enforcement is a later phase (see Phasing); the schema and the admin-app UI should assume it from the start.
2. The admin mini-app
A mini-app (per web/static/apps/README.md) that replaces the /admin console. Visible only to users with role=admin β the gate lives in the app framework (an app can declare adminOnly), not scattered through shell.js.
Surface (parity with today's admin.js, reachable in-app instead of on a separate page): invites, roster (remove/rename, now also roles), group name, server photo, contact email, user_invites toggle, bot config, test push. Companion-view shape (Chat | Interface) fits naturally: the Interface is the console; the Chat view is where admin-flavoured bot responses land (e.g. /invite already exists as a command).
Retirement is two-step: (1) ship the app, /admin stays; (2) once parity is proven in use, /admin becomes a redirect/pointer and the page + admin.js are deleted.
3. Server-admin onboarding (first run)
Entry
lchat add-instance <host> stops printing an /admin#token= link and instead prints a one-time setup link β a normal invite marked grants_admin (claiming it creates the membership and sets role=admin). The printed admin token / admin link remain available (lchat admin-link <host>) as the break-glass path.
Landing on the setup link does what invites do today: claim, generate the keypair, and β already true β the first member bootstraps the instance key client-side (shell.js, generateKey() when no holders exist). The admin lands in the chat view with a conversation with Eleven already open.
The setup script
Client-driven, keyed on: caller has role=admin and the server-side setup_complete config flag is unset. Steps, each one message + captured answer or tap-chip, each landing on an existing endpoint (now reachable with role-based auth):
- Your name β
postLabel(encrypted under the instance key, as today). - Group name β
POST /api/admin/group. - Contact email β
admin_emailinPOST /api/admin/settings(exists). - Server photo β
POST /api/admin/server-avatar(plaintext branding, as designed). Offered as a chip that opens the picker; skippable. - Enable AI? β a new instance setting (see Β§6). Presented honestly: on hosted instances it takes effect; on self-hosted it's a preference the sidecar respects when one exists.
- Can people invite other people? β
user_invites(exists). - Who is this space for? (#1374) β the plan question, moved here from the create flow so creating is never pay-gated. Web-only, and only when home's public
/api/pricebookanswers with tiers (native wrappers and self-hosted spaces never see it); a paid answer becomes a "Set up your plan" chip on the finish line that hands off to home's upgrade picker. The space runs free until that tap. - The key ceremony (next section) β deliberately near-last, so it's the note the setup ends on.
On completion the script sets setup_complete and hands over: "you can change any of this later β I've put an Admin app in your sidebar."
Answers are captured the /tutorial way β the script asks, and either a chip tap or the next composer submit is consumed as the answer (deterministic "extraction"; no parsing cleverness). The conversation itself is local notices (buildLocalNotice), not server messages: nothing needs to persist except the settings each step writes.
The key ceremony
One precision the copy must get right: what the admin must not lose is their credential bundle (private key + session), not "the instance key" per se. The instance key is re-wrappable by any other holder β but on a fresh one-member server they are the only holder, so losing their keys loses the server transitively. The script says so, sternly, in Eleven's voice: we cannot get this back for you, and that is the design working.
Offered paths, in order:
- Primary β "Save a passkey" (one click). The shipped phase-3a home-server enrolment popup: credentials into the vault blob, one Face ID. This is the same mechanism as the fragility banner's CTA; completing it here means the banner never appears for this user.
- Secondary β download a recovery kit. A small file (1Password Emergency-Kit style) for people who won't do passkeys: origin, display name, the private key, and a re-auth credential. New work + an open design question: session tokens are revocable, so the kit likely wants a durable server-side credential (a long-lived "recovery token" whose hash the server stores, exchangeable once for a fresh session β same hash-only-storage discipline as the admin token). Needs its own short design pass before building.
- Not offered: screenshots. Screenshots sync to cloud photo libraries in plaintext and are casually browsed and shared. Recommending one would undercut the seriousness the step is trying to convey. (A QR inside the recovery-kit file is possible later polish for cross-device import.)
The step is skippable β it's their server β but skipping routes into the existing fragility banner's double-dismiss discipline rather than silence.
4. User onboarding (joining from an invite)
Same dance, assembled almost entirely from shipped parts:
- Chat view first, no overlays. The join flow's name overlay goes away; claiming an invite lands directly in the chat view with Eleven's welcome conversation open.
- Welcome script (client-driven, keyed on first-open-after-join): 1. Name β
postLabel. 2. Avatar β existing avatar upload; chip opens the picker; skippable. 3. "Save your profile" β the phase-3a enrolment popup, one click and redirect. This is the post-join nudge fromonboarding-and-home-server.md, now delivered in Eleven's voice (one voice: the old backup toast is already slated for retirement; this replaces the banner's first appearance too, for joiners who complete it). 4. "Take the tour" β the existing chip into/tutorial. - Sidecar stand-down. On bot instances, sidecar-Eleven currently welcome-DMs new members. The client script owns onboarding; the sidecar's welcome DM is suppressed (or reduced to creating the DM thread the script runs in). Two Elevens must not greet the same person.
- Admin-editable welcome (later). The script reads an optional admin-authored welcome blob from config (written via the admin app) and plays it before/instead of the stock greeting. Cheap hook to leave in the script's structure now; UI later.
5. Roles vs identity: who is "the admin" in the script?
spaceInfo() already exposes an administrator identity to the tutorial. With roles on the roster this stops being config-only: the roster itself knows who the admins are (useful for "contact an admin" chips, the moderator table above, and the admin app's role management). admin_name/admin_email in settings remain the outward contact surface (they're branding-adjacent); role is the authorisation surface. Keep them distinct.
6. The AI toggle (honest scope)
"Enable AI on this server?" is a real setting with an operator-shaped dependency: AI is a sidecar deployment with an API key, not something an instance flag can conjure. Design:
- A new instance setting (e.g.
ai_enabled), admin-writable, readable by the sidecar. - The sidecar respects it: when off, Eleven runs deterministic-only (the existing per-bot kind machinery); when on β and a sidecar with a key exists β conversational AI is active.
- Hosted instances: flipping the toggle works end-to-end (sidecars are provisioned). Self-hosted without a sidecar: the setting records intent and the script/admin app says what's missing, pointing at
add-botdocs.
This slice gets its own small design pass before building (interaction with per-bot config, and whether "off" also silences AI for bots other than Eleven).
---
Phasing (each slice independently shippable)
- Roles β
users.role, first-claimer-is-admin, role-basedrequireAdminalternative, moderator in the enum (unenforced). Pure server + schema; no UI change. - Admin mini-app β
adminOnlyapp gate + the app itself over existing admin endpoints./adminuntouched. - First-run setup script β
grants_adminsetup invite fromadd-instance, the Eleven setup conversation,setup_completeflag. Ends with the passkey step only (recovery kit not yet built). - Key ceremony completion β recovery-kit design pass, then the download path.
- Joiner welcome script β name/avatar/save-profile/tour, join overlay removal, sidecar welcome stand-down.
- Moderator enforcement β the permission table in Β§1 enforced server-side; role management UI in the admin app.
- AI toggle β design pass +
ai_enabled+ sidecar respect. - Retire
/adminβ after the app proves parity in use.
Open decisions
- Recovery kit re-auth credential: durable recovery token (hash-only server storage) vs re-using the add-device machinery with a non-expiring variant. (Β§3, key ceremony β blocks phase 4 only.)
- Sidecar stand-down mechanism: suppress the welcome DM entirely, or keep the sidecar creating the DM thread and have the script play inside it?
- Does
grants_adminride on key-invites or user-invites? Setup is a key-invite shape (there's no inviter to wrap thread keys), but confirm against the invite-redesign fragment format (#e1.<T>). - Admin-editable welcome: plain text blob vs a tiny script format (chips?). Start with text.
- Moderator table details: can moderators cancel invites? See other members' invite links? Decide when enforcing (phase 6).