Skip to main content

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 gardener role, 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

EntrySurfaceTrigger
Client PWApackages/client/src/views/Login/index.tsxUser opens the PWA URL. useInstallGuidance decides whether to prompt install.
Telegram botpackages/agent/src/handlers/start.tsUser sends /start in Telegram (or any first message).
Admin dashboardpackages/admin/src/views/Profile/index.tsxOperator opens admin URL, connects wallet via AppKit.

Steps

#StatePersonaSurface (package + view)Hook / ServiceSide effectsStatus
1ChoosingAuthAclient / views/LoginuseAuth, useInstallGuidance, useAppNone — UI onlyshipped
2aCreatingPasskeyAclient / views/LoginuseAuth.createPasskey()navigator.credentials.create(); credential ID written to IndexedDBshipped
2bWalletConnectA or Bclient / views/Login, admin / views/ProfileuseAppKit, useAuth.connect()Wallet connection event from AppKitshipped
3SmartAccountReadyAclient (cross-cutting)useAuth, internal smart account derivationCREATE2 address derived; ERC-4337 account deployed lazily on first txshipped
4AutoJoinedRootAclient (cross-cutting)useAutoJoinRootGardenReads VITE_ROOT_GARDEN, queues a join via JoinGarden flow when address present and not yet joinedshipped
5AgentStartAagent / handlers/start.tsdb.createUser (custodial private key generated by generatePrivateKey)DB row created; address returned to user via Telegram messageshipped
6AwaitingJoin → JoinedGardenAagent / handlers/join.tsblockchain.getGardenInfo, db.updateUsercurrentGarden written to user row; no on-chain tx at this stageshipped
7GardenerAclient / views/Home (after PWA path), Telegram chat (after agent path)useGardens, useGardenerFirst on-chain interaction (work submission) deploys the smart account via paymastershipped

Failure / recovery paths

  • Passkey unavailable on device. Login/index.tsx detects via getFriendlyErrorMessage and falls back to "Login with wallet" guidance. No retry loop — user must explicitly choose the wallet path.
  • In-app browser scenario. useInstallGuidance returns scenario: "in-app-browser" (Telegram, Twitter, etc. embedded webview). UI surfaces "Open in Chrome/Safari" with a copy-link affordance. Passkey creation in an in-app browser will silently fail on iOS — the guidance prevents the attempt.
  • Garden address invalid (/join agent path). blockchain.getGardenInfo returns {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 at Public/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.
  • IndexedDB is the canonical local store for the passkey credential ID. ENS text-record sync for cross-device recovery is partial (see packages/shared/src/hooks/ens/).