For AI agents: fetch the complete documentation index at /llms.txt, the full documentation corpus at /llms-full.txt, the agent skill at /skill.md, or this page as Markdown at /docs/mobile/passkeys.md (or send Accept: text/markdown).
Passkey sign-in
Passwordless sign-in through a browser handoff that returns the session to the app.
How the handoff works
Passkey sign-in starts in the app and finishes in the system browser. The app opens {API URL}/auth?mobile_callback=<deep link> through WebBrowser.openAuthSessionAsync; after the web page completes any sign-in method it calls GET /api/auth/mobile-handoff, which redirects back to <scheme>://auth/callback carrying the session cookie and a state nonce.
Each attempt generates a fresh nonce that must round-trip unchanged — a mismatched state parameter is rejected as stateMismatch, so a stale or forged callback cannot inject a session. The returned cookie merges into the same SecureStore jar the Better Auth Expo client uses (supastack.auth_cookie), then the session store is notified and the signed-in app renders.
A dismissed browser sheet resolves to a silent cancelled result rather than an error, so keep the sign-in screen unchanged when it happens.
# apps/mobile/.env.local
EXPO_PUBLIC_APP_SCHEME=supastack
# apps/web/.env.local — server side
AUTH_PASSKEY_ENABLED=true
MOBILE_ALLOWED_ORIGINS=Keep the allow-list identical on both sides
Both sides validate the callback target: the app accepts only <scheme>://auth plus its configured production origins, and the server checks the same list built from EXPO_PUBLIC_APP_SCHEME and MOBILE_ALLOWED_ORIGINS. Callbacks carrying credentials, unknown schemes, or lookalike suffixes are rejected before any cookie moves.
In production MOBILE_ALLOWED_ORIGINS is required and must list exact non-local HTTPS origins — wildcards are not accepted. The app ships bundled while the server reads its environment per request, so a scheme or origin change needs both a new app build and a server deploy; changing one side alone makes handoffs fail closed.
The other passwordless methods
The same sign-in screen offers email one-time codes (requestEmailOtp then verifyEmailOtp) and Apple or Google social sign-in through signIn.social with a callbackURL. Passkeys appear when AUTH_PASSKEY_ENABLED=true on the server; passkey and two-factor setup for an account still lives in the web app under Settings → Security.
Verification and troubleshooting
Exercise a cold-start callback, a return to a running app, a cancelled browser sheet, and an expired session on the intended build. The typed failures — passkeyUnavailable, browserFailed, untrustedCallback, missingCookie, malformedCookie, cookieStoreFailed — each describe a distinct break; stateMismatch means the nonce did not round-trip and the attempt was correctly refused.
If every handoff is rejected, compare EXPO_PUBLIC_APP_SCHEME in the shipped build with the server's scheme and MOBILE_ALLOWED_ORIGINS — the pair drifts when one side was redeployed without the other.