Onboarding Journey
How a new participant arrives, gets an on-chain identity, and joins their first garden. Two parallel entry paths converge on the same Gardener smart account: the client PWA passkey path for users with smartphones, and the agent (Telegram/SMS) path for low-bandwidth field workers.
Personas
- A: Gardener — primary subject. Either path delivers a smart account, a
gardenerrole, and a current-garden assignment. - B: Operator — separate path. Operators authenticate via wallet (AppKit) directly into the admin dashboard and gain operator role from on-chain hat membership; they do not pass through the gardener onboarding flow.
State machine
Entry points
| Entry | Surface | Trigger |
|---|---|---|
| Client PWA | packages/client/src/views/Login/index.tsx | User opens the PWA URL. useInstallGuidance decides whether to prompt install. |
| Telegram bot | packages/agent/src/handlers/start.ts | User sends /start in Telegram (or any first message). |
| Admin dashboard | packages/admin/src/views/Profile/index.tsx | Operator opens admin URL, connects wallet via AppKit. |
Steps
| # | State | Persona | Surface (package + view) | Hook / Service | Side effects | Status |
|---|---|---|---|---|---|---|
| 1 | ChoosingAuth | A | client / views/Login | useAuth, useInstallGuidance, useApp | None — UI only | shipped |
| 2a | CreatingPasskey | A | client / views/Login | useAuth.createPasskey() | navigator.credentials.create(); with VITE_PASSKEY_SERVER_ENABLED=true, Pimlico hosted passkey server verifies the credential against normalized username/ENS context; localStorage caches metadata for same-device fallback | shipped |
| 2b | WalletConnect | A or B | client / views/Login, admin / views/Profile | useAppKit, useAuth.connect() | Wallet connection event from AppKit | shipped |
| 3 | SmartAccountReady | A | client (cross-cutting) | useAuth, internal smart account derivation | CREATE2 address derived; ERC-4337 account deployed lazily on first tx | shipped |
| 4 | AutoJoinedRoot | A | client (cross-cutting) | useAutoJoinRootGarden | Reads VITE_ROOT_GARDEN, queues a join via JoinGarden flow when address present and not yet joined | shipped |
| 5 | AgentStart | A | agent / handlers/start.ts | db.createUser (custodial private key generated by generatePrivateKey) | DB row created; address returned to user via Telegram message | shipped |
| 6 | AwaitingJoin → JoinedGarden | A | agent / handlers/join.ts | blockchain.getGardenInfo, db.updateUser | currentGarden written to user row; no on-chain tx at this stage | shipped |
| 7 | Gardener | A | client / views/Home (after PWA path), Telegram chat (after agent path) | useGardens, useGardener | First on-chain interaction (work submission) deploys the smart account via paymaster | shipped |
Failure / recovery paths
- Passkey recovery after local cache loss.
Login/index.tsxasks for the username or ENS handle before lookup. WithVITE_PASSKEY_SERVER_ENABLED=true, the shared auth service calls Pimlico's hosted passkey server using the same normalized context used at registration. Synced passkeys can recover where the user's passkey provider supports sync. Legacy local-only passkeys still work on the same device and may require re-enrollment after storage loss. - Passkey unavailable or server unavailable.
Login/index.tsxsurfaces retry/fallback guidance. The shared auth service preserves credential, username, auth mode, and expected-address metadata on server/network failure, then falls back to the same-device local credential when one exists. - Address mismatch. If server-discovered credential metadata rebuilds a different stored smart-account address, the shared auth service fails closed and the UI treats it as a recoverable failure. It does not silently create a new account.
- In-app browser scenario.
useInstallGuidancereturnsscenario: "in-app-browser"(Telegram, Twitter, etc. embedded webview). UI surfaces "Open in Chrome/Safari" with a copy-link affordance and blocks passkey ceremonies until the user opens a supported browser. - Garden address invalid (
/joinagent path).blockchain.getGardenInforeturns{exists: false}. Bot replies with "Garden not found" and does not mutate user record. - Custodial key compromise (agent path). No automated rotation today. The bot holds the private key; recovery is operationally manual. Spec § 3.1 calls out this limitation as accepted for the field-worker JTBD ("low bandwidth submission") — the trade-off is that gardeners do not manage keys themselves.
- Smart account first-tx failure. Paymaster funds exhausted, RPC unavailable, or bundler rejects the UserOp. Tx is retried via the standard mutation pipeline (
useTransactionSender). PWA does not deploy the smart account until needed (counterfactual deployment).
Connections
- Work submission (
./work-submission) — the next journey after onboarding for Persona A. The first work submission also deploys the smart account on-chain via the paymaster. - Funding (
./funding) — Persona D's onboarding to garden depositing reuses the same wallet-connect flow atPublic/Fund.tsx. - See also: the Passkey onboarding sequence diagram.
Notes for builders
- Hook boundary: all auth hooks live in
@green-goods/shared(hooks/auth/). Client and admin only consume them. - Single chain: the chain is fixed at build time by
VITE_CHAIN_ID. There is no chain-switching UX during onboarding. - LocalStorage is a cache and same-device fallback for passkey credential metadata, normalized username, RP ID, and expected smart-account address. It is not the recovery source of truth when the hosted server path is enabled.
VITE_PASSKEY_SERVER_ENABLED=falseis the rollback path and keeps legacy local-only passkey behavior.- Green Goods does not currently import legacy local-only credentials into Pimlico's hosted passkey server, and this lane does not promise same-address recovery after total passkey-provider loss.