You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Answer "what happens when all passkeys are lost" without falling back to passwords, and
hold the items the team deliberately parked. The recovery design must be signed off
before Phase B GA; the implementation can follow.
What's included
Recovery
C1 — Recovery design sign-off. Email-OTP to a short-lived privileged session whose only permitted action is enrolling a new passkey; admin-sent registration link as backstop; recovery codes deferred. Required before Phase B GA.No longer gates Phase B GA — 2026-09-07. Phase B's scope is the signup flow and only that; a Phase C design cannot gate a Phase B release. C1's priority is now set by how long a user without a usable passkey has to wait, not by a release date.
C2 — Interim recovery ships with Phase B: email-OTP login as a permanent fallback.SUPERSEDED 2026-09-07 — decided, not deferred.Email-OTP is a recovery ceremony, never a login method. Login is always passkey. A permanent OTP login fallback moves the account's real security floor from the passkey to the email inbox — the thing passwordless exists to avoid — so the interim is skipped and C1/C3's enroll-only session is the whole answer. #128 hides both /login entry points behind one flag. The /error landing for an otpEmail-only account is intended, not a defect — do not "fix" it by re-exposing OTP sign-in. The machinery stays intact (app/resources/otp/, intent=email-link, /login/verify/email) and is what C3 builds on.
C3 — Dedicated recovery flow + emails/user-account-recovery.tsx. Three classes to serve (named 2026-09-07):
(a) Lost authenticator — device wiped, lost or stolen. Zitadel still holds the passkey record; the human does not. The largest class after GA, and the only one that hits a user who did everything right.
(b) Verified but never enrolled — abandoned at the passkey step. Same population as C9.
(c) Admin backstop — a support-triggered passkey registration link for what self-serve cannot reach. Net-new work: nothing in our code sends one today; zitadel-provider exposes ListPasskeys and nothing else.
Not a class: removing a last passkey — the server-side guard in passkeys.service.ts:141 blocks it.
C9 — The stranded class: verified email, no passkey.zp#134, gap 2. AbandonedUserGC.isAbandoned returns false the moment IsEmailVerified is true and never consults passkey state, so an account that verifies its email and then abandons at the passkey step can neither authenticate nor be collected — stranded permanently. It belongs to this phase because the fix is a C1 question: if recovery makes these accounts reachable, deleting them at the 30-day cutoff destroys an account the user could still claim; if it does not, collection is the only outcome left. So the GC predicate — no verified email or no usable credential — cannot be settled before C1 signs off. The issue's option B (hold the Zitadel user inactive until signup completes) is a C1 review item, not a separate track: it carries an untested deadlock (can a deactivated user still verify and enroll?) and reuses the state platformaccess_controller.go already spends on policy suspension. Live today, not theoretical:#128 hides email-OTP sign-in, and an account whose only enrolled method is otpEmail — exactly this class — reaches /error.
Parked — need a team decision
True usernameless sign-in (resolving a random credential ID to a user) — blocked on Zitadel #8899. The hint-based fast path shipped in Phase A; this is the remaining capability.
Org-level auth policy (require passkey/MFA per org)
Adoption and health dashboards (method mix, enrollment funnel, ceremony failure rate)
Bot-protection tooling choice — decision deadline: before Phase B GA. Placement fixed: signup and recovery forms only.
Playwright virtual-authenticator E2E
Passkey last-used timestamps (created-at shipped in Phase A)
Status
Not started. C1 needs scheduling — it blocks Phase B GA, not Phase B start.
Updated 2026-09-07 — the phase boundary is now explicit.Phase B ships the signup flow; Phase C owns every recovery path and every abandoned-account decision. Neither gates the other. Phase B's operating assumption is that a user who signs up by email enrols a passkey; a user who ends up without a usable one reaches /error and waits for this phase.
That wait is the accepted residual and it is what sets this phase's priority. C6 resolved via reCAPTCHA v3 (#127); C2 superseded; C1 retired as a release gate; C9 added from the Phase B review and still downstream of C1 — C2's removal is precisely what keeps that dependency real, since with no OTP login fallback a verified-no-passkey account genuinely is unreachable until C3 ships.
Gap 1 of zp#134 — the abandoned-account GC never leaving dry-run — is deliberately not in this phase. It is Phase B Lane F F3 plus §5 rollout step 8 (--abandoned-account-gc-dry-run=false); it is unwired because F3 has not merged, not because it is unowned.
Updated 2026-09-14 — implementation complete; in team review. All lanes are built, machine-reviewed and CI-green: 19 PRs across six repos — 2 merged, 12 open for review, 4 drafts held for the rollout, 1 closed (superseded by a stack). Two changes came out of review: the email-verified signal became a typed status field status.emailVerification (Verified | Unverified, empty = not yet synced) instead of a condition, landed across milo, zitadel-provider and staff-portal; and the recovery mail copy was simplified per content review (email-templates#108, merged, on staging since 09-11). Merge order (roadmap §5a): auth-ui #130 → #134 → milo #782 (+ tag) → zitadel-provider #141 → #136 → #137 → #138 → #139 → #140 → #142 (#143 standalone) → milo #784; then the infra drafts and staff-portal for the staging rollout; production per §5b.
Sub-issue of datum-cloud/enhancements#738. Phase C of the passkey program.
Scope
Answer "what happens when all passkeys are lost" without falling back to passwords, and
hold the items the team deliberately parked. The recovery design must be signed off
before Phase B GA; the implementation can follow.
What's included
Recovery
Required before Phase B GA.No longer gates Phase B GA — 2026-09-07. Phase B's scope is the signup flow and only that; a Phase C design cannot gate a Phase B release. C1's priority is now set by how long a user without a usable passkey has to wait, not by a release date.C2 — Interim recovery ships with Phase B: email-OTP login as a permanent fallback.SUPERSEDED 2026-09-07 — decided, not deferred. Email-OTP is a recovery ceremony, never a login method. Login is always passkey. A permanent OTP login fallback moves the account's real security floor from the passkey to the email inbox — the thing passwordless exists to avoid — so the interim is skipped and C1/C3's enroll-only session is the whole answer. #128 hides both/loginentry points behind one flag. The/errorlanding for anotpEmail-only account is intended, not a defect — do not "fix" it by re-exposing OTP sign-in. The machinery stays intact (app/resources/otp/,intent=email-link,/login/verify/email) and is what C3 builds on.emails/user-account-recovery.tsx. Three classes to serve (named 2026-09-07):ListPasskeysand nothing else.passkeys.service.ts:141blocks it.AbandonedUserGC.isAbandonedreturnsfalsethe momentIsEmailVerifiedis true and never consults passkey state, so an account that verifies its email and then abandons at the passkey step can neither authenticate nor be collected — stranded permanently. It belongs to this phase because the fix is a C1 question: if recovery makes these accounts reachable, deleting them at the 30-day cutoff destroys an account the user could still claim; if it does not, collection is the only outcome left. So the GC predicate —no verified emailorno usable credential— cannot be settled before C1 signs off. The issue's option B (hold the Zitadel user inactive until signup completes) is a C1 review item, not a separate track: it carries an untested deadlock (can a deactivated user still verify and enroll?) and reuses the stateplatformaccess_controller.goalready spends on policy suspension. Live today, not theoretical: #128 hides email-OTP sign-in, and an account whose only enrolled method isotpEmail— exactly this class — reaches/error.Parked — need a team decision
Status
Not started. C1 needs scheduling — it blocks Phase B GA, not Phase B start.
Updated 2026-09-07 — the phase boundary is now explicit. Phase B ships the signup flow; Phase C owns every recovery path and every abandoned-account decision. Neither gates the other. Phase B's operating assumption is that a user who signs up by email enrols a passkey; a user who ends up without a usable one reaches
/errorand waits for this phase.That wait is the accepted residual and it is what sets this phase's priority. C6 resolved via reCAPTCHA v3 (#127); C2 superseded; C1 retired as a release gate; C9 added from the Phase B review and still downstream of C1 — C2's removal is precisely what keeps that dependency real, since with no OTP login fallback a verified-no-passkey account genuinely is unreachable until C3 ships.
Gap 1 of zp#134 — the abandoned-account GC never leaving dry-run — is deliberately not in this phase. It is Phase B Lane F
F3plus §5 rollout step 8 (--abandoned-account-gc-dry-run=false); it is unwired because F3 has not merged, not because it is unowned.Updated 2026-09-14 — implementation complete; in team review. All lanes are built, machine-reviewed and CI-green: 19 PRs across six repos — 2 merged, 12 open for review, 4 drafts held for the rollout, 1 closed (superseded by a stack). Two changes came out of review: the email-verified signal became a typed status field
status.emailVerification(Verified|Unverified, empty = not yet synced) instead of a condition, landed across milo, zitadel-provider and staff-portal; and the recovery mail copy was simplified per content review (email-templates#108, merged, on staging since 09-11). Merge order (roadmap §5a): auth-ui #130 → #134 → milo #782 (+ tag) → zitadel-provider #141 → #136 → #137 → #138 → #139 → #140 → #142 (#143 standalone) → milo #784; then the infra drafts and staff-portal for the staging rollout; production per §5b.bun audit/recover,/recover/complete)status.emailVerificationfield,PasskeyRegistrationLinktype, rolesCreatePasskeyRegistrationLink,ListAuthMethodTypesemailVerificationfield at provisioning, email events, sweeper/v1/email/recoverywebhookPasskeyRegistrationLinkREST (create-only, flagged)