# 🤖 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
  `/admin` console, no forms-first overlay. `/admin` is 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 has `role=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/revoke `admin` (and
  `moderator`) 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):

1. **Your name** → `postLabel` (encrypted under the instance key, as today).
2. **Group name** → `POST /api/admin/group`.
3. **Contact email** → `admin_email` in `POST /api/admin/settings` (exists).
4. **Server photo** → `POST /api/admin/server-avatar` (plaintext branding, as
   designed). Offered as a chip that opens the picker; skippable.
5. **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.
6. **Can people invite other people?** → `user_invites` (exists).
7. **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/pricebook` answers 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.
8. **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:

1. **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.
2. **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.
3. **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 from `onboarding-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-bot` docs.

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)

1. **Roles** — `users.role`, first-claimer-is-admin, role-based
   `requireAdmin` alternative, moderator in the enum (unenforced). Pure
   server + schema; no UI change.
2. **Admin mini-app** — `adminOnly` app gate + the app itself over existing
   admin endpoints. `/admin` untouched.
3. **First-run setup script** — `grants_admin` setup invite from
   `add-instance`, the Eleven setup conversation, `setup_complete` flag.
   Ends with the passkey step only (recovery kit not yet built).
4. **Key ceremony completion** — recovery-kit design pass, then the
   download path.
5. **Joiner welcome script** — name/avatar/save-profile/tour, join overlay
   removal, sidecar welcome stand-down.
6. **Moderator enforcement** — the permission table in §1 enforced
   server-side; role management UI in the admin app.
7. **AI toggle** — design pass + `ai_enabled` + sidecar respect.
8. **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_admin` ride 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).
