Skip to content
authentication

Passkeys in Next.js: A CIAM Recipe for App Router

Updated 2026-08-19 · 8 min read · By @guptadeepak

Key takeaways

  • Do not implement WebAuthn from scratch in a Next.js app. Use a CIAM SDK and own RP-ID, recovery, and enrollment UX.
  • Set RP-ID to the apex domain. app.example.com as RP-ID will break www and marketing hosts.
  • Call getClientCapabilities() before navigator.credentials.create. Conditional create needs an explicit check.
  • Clerk and Auth0 have the fastest Next.js time-to-first-login. MojoAuth and Stytch have native passkeys. Descope is orchestration, not a passkey product.
  • Recovery cannot be email magic link only if the account is otherwise passkey-only.

This is a recipe, not a crypto lesson. If you need the protocol, read WebAuthn explained and Level 3.

Pick the vendor before you pick the helper library

If you needStart here
Fastest Next.js UIClerk
B2C + B2B SSO, broad SDKsAuth0
Passkey adoption as the goalMojoAuth or Stytch
You already decided to buildSimpleWebAuthn + your own session store. Budget recovery.

Do not install @simplewebauthn/browser and a CIAM SDK. Pick one stack.

The four settings that break production

  1. RP-ID. Apex example.com, not app.example.com. Related origins if you also run example.co.uk. See Level 3.
  2. Challenge. Server-issued, one-time, ~5 minutes, bound to the login session.
  3. User verification. Prefer required for registration. preferred hides devices that cannot UV and then you debug ghosts.
  4. Recovery. A passkey-only account recovered by email is an email-authenticated account. Design it on purpose: account recovery.

App Router shape

Keep WebAuthn ceremony on the server:

  • POST /api/webauthn/register/options and /register/verify
  • POST /api/webauthn/login/options and /login/verify
  • Client only calls navigator.credentials.create / get and posts the JSON back

If the CIAM vendor hosts the login box (Clerk <SignIn />, Auth0 Universal Login, Stytch SDK), you do not own those routes. You own RP-ID configuration in their dashboard and the recovery policy.

Conditional UI

On the sign-in page, if getClientCapabilities() reports conditionalGet, use mediation: "conditional" so the passkey appears in autofill. If conditionalCreate is true and the user just signed in with a saved password, request a silent create. Check the capability first. Apple and Chrome both say so.

What to demo before you ship

A user on iOS Safari with iCloud Keychain, a user on Chrome with Google Password Manager, a user with a YubiKey, and a user who deleted the passkey. If the last case is "we emailed a magic link," write that down in the threat model. It is not a passkey-secured account.

Related vendors

Where to next

FAQ

Should I use SimpleWebAuthn instead of a CIAM?
Only if you have already decided to build CIAM. SimpleWebAuthn is the right library for that path. For a product team, Clerk, Auth0, Stytch, or Descope will own attestation, recovery, and browser quirks you do not want to staff.
Which CIAM is fastest on Next.js App Router?
Clerk, then Auth0. If passkey adoption is the reason you are here, evaluate Stytch or Descope even if the first login takes a day longer.
Where does the challenge get stored?
On the server, bound to the session, single-use, short TTL. Never mint a WebAuthn challenge in a Client Component and trust it round-trip without server verification.
Does this work on localhost?
WebAuthn allows localhost as a secure context. Production RP-ID must be the real apex. Do not register localhost credentials into a production user store.

Sources

  • W3C WebAuthn Level 3
  • Next.js App Router documentation
  • CIAM Compass passkey orchestration ranking, 19 August 2026
Last reviewed 2026-08-19.