Skip to main content

Passkeys

Gardeners should never need a seed phrase to plant trees. Passkey authentication signs people in with the platform authenticator they already have (Face ID, a fingerprint, a device PIN) and backs their identity with a smart account, so field workers get real on-chain records without wallet onboarding. Wallets remain available for stewards and power users; passkeys are the default path.

The admin sign-in screen: a filled Create Account button with a key icon, an outlined Connect Wallet button, and a link for people who already have an account
Sign in with a passkey account or connect an existing wallet.

How it flows​

A gardener signs with their device, and Green Goods sponsors the gas, so there's no wallet to set up.

Where it lives in the code​

LayerSource
Passkey server selectionpackages/shared/src/config/passkeyServer.ts
Auth hooks and providerspackages/shared/src/hooks/auth
Runtime wiring and the dev bypasspackages/client/src/routes/WalletRuntimeProviders.tsx

Authentication branches through the shared auth APIs everywhere; no surface implements its own sign-in. One behavior worth knowing: signing in is not the same as joining a garden, and the two recover independently.

Which sites a passkey works on​

A browser offers a passkey only for the domain it was created under. Whoever issues the sign-up options chooses that domain, so the passkey server decides where an account can sign in.

  • The Green Goods passkey directory (the agent route /public/passkeys/rpc) issues every passkey under greengoods.app, so one account signs in on every Green Goods site. Deployed builds use it by default at the existing VITE_API_BASE_URL, and the agent always serves it. The agent reuses its Pimlico API key to check names registered on the hosted server.
  • The hosted passkey server issues each site its own hostname, so an account made there signs in only on the site that created it. Accounts made before the directory stay on it and sign in the same way as before.

Sign-in asks the directory first and falls back to the hosted server for a name the directory does not hold. The directory refuses a name the hosted server already holds, which keeps one owner per name across both.

The sites the directory answers are the agent's allowed origins (AGENT_ALLOWED_ORIGINS; the production list that bun run dev:smoke -- prod expects lives in scripts/dev/smoke-prod.js). Every directory RPC and its preflight require an explicitly listed origin, and the public API's automatic Vercel-preview exception does not apply here, so adding a subdomain does not approve it. Of the listed origins, only https pages under greengoods.app are issued a passkey. The app's own check that a page sits under the passkey domain (classifyPasskeyCeremonyContext) is a first filter, not the list.

Working with it locally​

You rarely need a real passkey ceremony in development. The dev-only bypass from Getting Started (?mockAuth=steward&presentation=pwa) signs you in as a test identity. The Playwright passkey project (tests/specs/client.passkey.spec.ts) drives a virtual authenticator and expects its strict server substitute to reject the credential, leaving the user signed out after reload. The reporting-account passkey specs exercise recovery against local substitutes and stop before signing. tests/specs/client.session-navigation.ci.spec.ts checks mock-auth sessions and navigation; it does not prove a wallet or passkey ceremony. Real-device biometrics, remote passkey services, account creation on a chain and sponsored transactions still need authenticated device QA.

Local development keeps passkeys local-only unless VITE_PASSKEY_SERVER_ENABLED=true enables the local agent directory. VITE_PASSKEY_RP_ID is a local-development override; deployed sites, including beta.greengoods.app and beta.admin.greengoods.app, use greengoods.app.

Canonical upstream docs​

Next steps​