eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Signup-first join page β€” design
2
3**Date:** 2026-07-31 Β· **Status:** approved by Paul Β· **Scope:** web join page
4(native follows as a stated follow-up)
5
6## What changes
7
8Today a joiner opening `/join/<name>#<secret>` sees the app/web choice first,
9claims an invite with no account, and is asked to sign in only afterwards β€”
10by Eleven's welcome conversation, then the fragility banner. This flips the
11order: the **first screen is signup/sign-in**, and by the time the joiner
12reaches the continue-on-web / continue-in-the-app choice they already have an
13Eleven account and passkey. This is the golden path
14`docs/onboarding-and-home-server.md` Β§1 designed but never built.
15
16The account gate is **offered first, skippable** β€” a quiet "continue without
17an account" escape preserves the zero-friction join; skippers fall through to
18today's exact flow, including Eleven's ask and the fragility banner.
19
20## 1. The lead card
21
22New first screen for fresh joiners β€” all fresh-person shapes: generic
23invites, DM/user invites, and setup (new-space) invites β€” rendered by
24`join.js` from a new template in `join.html`:
25
26- Branded like today's welcome card (space avatar + name via the existing
27 sealed-branding machinery; plaintext branding fallback).
28- Copy: "You've been invited to *(space)*" plus one explainer line β€” your
29 sign-in makes your access portable to any device.
30- Primary: **Continue with your Eleven sign-in** β†’ same-tab round-trip to the
31 home server (Β§3).
32- Quiet link: **Continue without an account** β†’ reveals today's welcome card
33 unchanged (Open in the app / Get the app / Continue in the browser, with
34 the browser-only caveat).
35
36"All the sign-in options" are satisfied by landing on home's existing doors
37surface (smart single door β€” last-used else platform default β€” plus "Other
38ways to sign in": passkey, Apple, Google, Microsoft, GitHub, email code). The
39lead card links to that surface; it does not rebuild it.
40
41## 2. Who never sees the lead card
42
43These paths fall through to today's behaviour untouched:
44
45- Add-device links (`payload.priv`) β€” the person already has an identity.
46- Already-a-member adopt paths (all shapes).
47- The native embed form path (`window.__lchatEmbed`) and the Mac webview.
48- Dead/consumed invites β€” the existing error card (with its sign-in door).
49- Instances with no home server configured (`__HOME_ORIGIN__` empty β€”
50 self-hosters).
51
52## 3. The home-side pre-join intent
53
54Home's sign-in page (`home/`) grows a lightweight pre-join mode:
55
56- Query params: `?return=<url>` plus the instance host for display (host and
57 group name are plaintext branding by design β€” CLAUDE.md Β§ branding).
58- Any door works. After the account exists **and a passkey is saved**, home
59 redirects to the return URL. An account that already exists in this browser
60 session completes immediately (the shipped session-reuse behaviour,
61 `d2c4092`).
62- **Return-URL guard:** must be `https` and path `/join/…`. Instance hosts
63 are arbitrary custom domains, so no host allowlist is possible; home
64 displays the destination host so the person sees where they're being sent.
65 This is post-auth navigation, not a credential redirect, so the residual
66 open-redirect risk is display-mitigated, not allowlist-mitigated.
67
68## 4. The invite secret never travels
69
70- Before navigating to home, `join.js` stashes the full original `#fragment`
71 in **instance-origin localStorage**, keyed by invite id, with an expiry,
72 cleared on restore. localStorage (not sessionStorage) so a mobile OAuth
73 app-switch that returns in a fresh tab can't orphan the secret. Lingering
74 risk is bounded: entries expire, and the invite is one-time anyway.
75- The `return` URL carries **no fragment**. On return, `join.js` restores the
76 fragment from the stash and cleans the URL (`history.replaceState`).
77- The home origin never sees the invite secret, preserving the invariant
78 that fragments never reach any server and the secret stays on the instance
79 origin.
80
81## 5. Return: the caveat-free choice card
82
83Back on the join page with a signed-in marker (query flag from home, cleaned
84after read):
85
86- The joiner sees the **same web/app choice**, but the "this browser only"
87 caveat is replaced with a positive line: "Your access is saved to your
88 Eleven account β€” you can sign in from any device."
89- **Continue in the browser** claims exactly as today β€” keypair, deferred
90 claim, bind-MAC (the claim-on-button-press invariant is untouched) β€” then,
91 before `location.href = "/"`, `join.js` auto-runs the existing `/enroll`
92 round-trip (`launchEnrol` machinery). Against the live home session this
93 completes instantly with no second ceremony, so the space is genuinely
94 saved to the account before the joiner lands in the chat.
95- **Open in the app** keeps today's `elevenmessenger://add` scheme handoff.
96 The vault-first handoff ("the space is already in your account β€” just sign
97 in in the app") is stated **phase 2**, because it changes invite-consumption
98 timing (claim-at-signup) and touches native sign-in.
99
100## 6. Eleven stands down
101
102- The welcome flow's `access` step (`web/static/welcome.flow.js`) gains an
103 "already saved" skip condition alongside its existing `native` /
104 `canEnrol` skips; likewise the `finish` step's "you haven't signed in yet"
105 warning and chip.
106- The fragility banner never arms for a saved session (existing behaviour β€”
107 verify, don't rebuild).
108
109## 7. Testing
110
111- Pull the which-card-for-which-state decision (link shape Γ— home-origin
112 presence Γ— signed-in marker Γ— embed) out of DOM code into a pure helper
113 (`ui.js` or a new module) with Node tests, per the testable-module
114 convention.
115- Go test on home for the `?return` guard (https-only, `/join/` path only).
116- `node --check` on touched JS; `npm test` + `go test ./...` green.
117- Manual verification per the developer's recorded preference
118 (`CLAUDE.local.md`; ask if unrecorded).
119- Changelog entry (user-visible web change).
120
121## Out of scope (stated)
122
123- Native (iOS/Mac) direct-join restructure β€” follow-up, building on
124 `SignInWithEleven`.
125- Claim-at-signup + vault-first app handoff β€” phase 2.
126- Any change to invite consumption timing or the deferred-claim invariant.
127- The dead `homescreen-tpl` markup in `join.html` may be deleted in passing
128 if touched, but its removal is not a goal of this change.
129