eleven / ocho Public
π€ Signup-first join page β design
Date: 2026-07-31 Β· Status: approved by Paul Β· Scope: web join page (native follows as a stated follow-up)
What changes
Today a joiner opening /join/<name>#<secret> sees the app/web choice first, claims an invite with no account, and is asked to sign in only afterwards β by Eleven's welcome conversation, then the fragility banner. This flips the order: the first screen is signup/sign-in, and by the time the joiner reaches the continue-on-web / continue-in-the-app choice they already have an Eleven account and passkey. This is the golden path docs/onboarding-and-home-server.md Β§1 designed but never built.
The account gate is offered first, skippable β a quiet "continue without an account" escape preserves the zero-friction join; skippers fall through to today's exact flow, including Eleven's ask and the fragility banner.
1. The lead card
New first screen for fresh joiners β all fresh-person shapes: generic invites, DM/user invites, and setup (new-space) invites β rendered by join.js from a new template in join.html:
- Branded like today's welcome card (space avatar + name via the existing sealed-branding machinery; plaintext branding fallback).
- Copy: "You've been invited to (space)" plus one explainer line β your sign-in makes your access portable to any device.
- Primary: Continue with your Eleven sign-in β same-tab round-trip to the home server (Β§3).
- Quiet link: Continue without an account β reveals today's welcome card unchanged (Open in the app / Get the app / Continue in the browser, with the browser-only caveat).
"All the sign-in options" are satisfied by landing on home's existing doors surface (smart single door β last-used else platform default β plus "Other ways to sign in": passkey, Apple, Google, Microsoft, GitHub, email code). The lead card links to that surface; it does not rebuild it.
2. Who never sees the lead card
These paths fall through to today's behaviour untouched:
- Add-device links (
payload.priv) β the person already has an identity. - Already-a-member adopt paths (all shapes).
- The native embed form path (
window.__lchatEmbed) and the Mac webview. - Dead/consumed invites β the existing error card (with its sign-in door).
- Instances with no home server configured (
__HOME_ORIGIN__empty β self-hosters). - π€ Setup links opened from home's create-a-space flow (
?src=home): that person signed in at home one tap ago, and home has already saved the new space to their account. The sign-in they still owe β adding this space's KEY to that account β is the setup conversation'skeys-homestep, at the end.
3. The home-side pre-join intent
Home's sign-in page (home/) grows a lightweight pre-join mode:
- Query params:
?return=<url>plus the instance host for display (host and group name are plaintext branding by design β CLAUDE.md Β§ branding). - Any door works. After the account exists and a passkey is saved, home redirects to the return URL. An account that already exists in this browser session completes immediately (the shipped session-reuse behaviour,
d2c4092). - Return-URL guard: must be
httpsand path/join/β¦. Instance hosts are arbitrary custom domains, so no host allowlist is possible; home displays the destination host so the person sees where they're being sent. This is post-auth navigation, not a credential redirect, so the residual open-redirect risk is display-mitigated, not allowlist-mitigated.
4. The invite secret never travels
- Before navigating to home,
join.jsstashes the full original#fragmentin instance-origin localStorage, keyed by invite id, with an expiry, cleared on restore. localStorage (not sessionStorage) so a mobile OAuth app-switch that returns in a fresh tab can't orphan the secret. Lingering risk is bounded: entries expire, and the invite is one-time anyway. - The
returnURL carries no fragment. On return,join.jsrestores the fragment from the stash and cleans the URL (history.replaceState). - The home origin never sees the invite secret, preserving the invariant that fragments never reach any server and the secret stays on the instance origin.
5. Return: the caveat-free choice card
Back on the join page with a signed-in marker (query flag from home, cleaned after read):
- The joiner sees the same web/app choice, but the "this browser only" caveat is replaced with a positive line: "Your access is saved to your Eleven account β you can sign in from any device."
- Continue in the browser claims exactly as today β keypair, deferred claim, bind-MAC (the claim-on-button-press invariant is untouched) β then, before
location.href = "/",join.jsauto-runs the existing/enrollround-trip (launchEnrolmachinery). Against the live home session this completes instantly with no second ceremony, so the space is genuinely saved to the account before the joiner lands in the chat. - Open in the app keeps today's
elevenmessenger://addscheme handoff. The vault-first handoff ("the space is already in your account β just sign in in the app") is stated phase 2, because it changes invite-consumption timing (claim-at-signup) and touches native sign-in.
6. Eleven stands down
- The welcome flow's
accessstep (web/static/welcome.flow.js) gains an "already saved" skip condition alongside its existingnative/canEnrolskips; likewise thefinishstep's "you haven't signed in yet" warning and chip. - The fragility banner never arms for a saved session (existing behaviour β verify, don't rebuild).
7. Testing
- Pull the which-card-for-which-state decision (link shape Γ home-origin presence Γ signed-in marker Γ embed) out of DOM code into a pure helper (
ui.jsor a new module) with Node tests, per the testable-module convention. - Go test on home for the
?returnguard (https-only,/join/path only). node --checkon touched JS;npm test+go test ./...green.- Manual verification per the developer's recorded preference (
CLAUDE.local.md; ask if unrecorded). - Changelog entry (user-visible web change).
Out of scope (stated)
- Native (iOS/Mac) direct-join restructure β follow-up, building on
SignInWithEleven. - Claim-at-signup + vault-first app handoff β phase 2.
- Any change to invite consumption timing or the deferred-claim invariant.
- The dead
homescreen-tplmarkup injoin.htmlmay be deleted in passing if touched, but its removal is not a goal of this change.