eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

πŸ€– 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).

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 https and 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.js stashes the full original #fragment in 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 return URL carries no fragment. On return, join.js restores 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.js auto-runs the existing /enroll round-trip (launchEnrol machinery). 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://add scheme 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 access step (web/static/welcome.flow.js) gains an "already saved" skip condition alongside its existing native / canEnrol skips; likewise the finish step'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.js or a new module) with Node tests, per the testable-module convention.
  • Go test on home for the ?return guard (https-only, /join/ path only).
  • node --check on 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-tpl markup in join.html may be deleted in passing if touched, but its removal is not a goal of this change.