# 🤖 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's `keys-home` step, 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 `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.
