Passkey Authentication Service

Architecture Views

21 views, in reading order. Every view ships three ways: an HTML page, an SVG that re-opens in diagrams.net fully editable, and draw.io source.

The "sign in with a passkey" button, from the side that has to make it true. Read the set in order: the first two views draw the boundary, views 3 to 6 say who it is for and the exact moment each of them stops trusting it, 7 to 9 are the structure, 10 to 12 the data, 13 to 15 what happens at runtime, 16 to 19 how it is run, and 20 to 21 why it is safe. Every number in the package is a stated assumption, chosen so a reviewer can argue with it.

Context and scope

What the platform verifies, what it refuses to own, and why those refusals are the architecture rather than modesty.

People and journeys

Who signs in, who runs a tenant, and the three moments where a passwordless account is at its most fragile.
03 The people signing in Everyday user 180 M accounts Goal — Sign in on my phone without remembering anything, and still get back in when I drop it in a lake. Core journeys Enrol a first passkey after a password login Sign in on a new device Get back in with no devices the expensive one Multi-device user 2.3 credentials average Goal — Add my work laptop's security key without being asked to set everything up again. Core journeys Add a second credential Revoke a lost device The people who run a tenant Tenant administrator 40 tenants Goal — Turn the password off without my support queue exploding on Monday morning. Core journeys Turn off the password Change an authentication policy Support agent 320 seats Goal — Prove this caller is who they say they are without becoming the account's real authenticator. Core journeys Run an assisted recovery On-call engineer 1 primary, 1 secondary Goal — Tell a ceremony-store blip apart from a tenant that mistyped its origin, before the pager escalates. Core journeys Triage a login incident Machines and standards Relying-party backend 40 integrated Goal — Hand me a verdict I can verify offline, and let me keep issuing my own sessions. Core journeys Exchange a verdict for a session Metadata ingest job daily, signature-checked Goal — Refuse an unsigned metadata update rather than accept a fresher one. Core journeys Refresh the metadata snapshot Authenticator platform or roaming Goal — Never hand the private key to anyone, this platform included. Core journeys Sign an assertion Refuse a duplicate credential Passkey Authentication Service — Actors and Their Core Journeys Person or role Journey / task External / third party Security / platform Eight parties, three of them machines. The authenticator is in the cast because the one secret that matters never leaves it. v 1.0 · owner Security & Identity Architecture · date 2026-10 Actors and Their Core Journeys Eight parties, three of them machines, and the goal each would state in their own words. HTML page SVG draw.io

Structure

The planes, the one mutable store on the hot path, and the interfaces either side of it.

Data

What is held, which of it can be lost and rebuilt, and why the credential store holds nothing worth stealing.

Runtime

One ceremony end to end, how a credential is admitted, and what each recovery route actually proves.

Operations

Where it runs, how a policy change reaches production, what is watched, and the loop a credential travels.

Assurance

The trust zones, and the boundary between this platform's verdict and the tenant's session.

Architecture One-Pager

The problem, the shape of the answer, the decisions that carry it, and what a prototype should prove.

Ceremony state is ephemeral and region-pinned; credential state is durable and globally replicated; the authentication hot path touches nothing else.

Every major consumer service now offers to sign you in with a passkey, and the cryptography behind that button is settled: WebAuthn specifies the ceremony, the authenticator holds a private key that never leaves it, and a phishing site cannot produce a valid assertion for a domain it does not control. None of that is the hard part. The hard part is everything the specification deliberately leaves to the relying party, and it is where passwordless deployments actually fail. Where does the five-minute challenge live, and what consumes it exactly once? What happens to the user who has one passkey and drops their phone in a lake — in a system that has, by design, no password to reset? Is a signature counter that moved backwards a cloned authenticator or a synced passkey behaving exactly as its vendor intended, and which answer locks out more people? When does a platform get to say the password is gone, and who absorbs the support calls that claim produces? This package is the architecture for a multi-tenant service that answers those questions for forty relying parties, at a scale where each answer has a cost somebody pays.

The design rests on one boundary. The ceremony — a challenge written, read once and discarded inside five minutes — is region-pinned, strongly consistent and never replicated. The credential record — a public key that must outlive every component in the system — is globally replicated, read-mostly, and read-only from the login path. Nothing else on that path is a durable write, which is what makes a 60 ms p99 reachable with no distributed transaction anywhere in a login, and what makes every dependency's failure a declared degradation rather than an outage: credential store unavailable fails closed, risk engine unavailable fails open with the verdict marked unevaluated, metadata feed unavailable fails static on the last signed snapshot. Authenticator trust is decided once at registration and written down as an immutable verdict, so the authentication path is pure cryptography against a recorded decision and the metadata feed is not a login dependency. The platform returns a signed, short-lived, single-use verdict and never issues a session, which bounds what a compromise of it costs a tenant. And because a passwordless account's real security is set by whichever route gets a locked-out user back in, every recovery route carries an assurance classification into the verdict, must end with a working credential, and earns the new credential a cooling-off period before it may revoke anything the attacker would want gone.

What it is, and what it is not

A verifier of assertions against recorded decisionsAn evaluator of policy on every login
A holder of public keys and verification materialA custodian of authentication secrets
An issuer of short-lived signed verdictsAn identity provider that issues sessions
Two stores split by the lifetime of their dataOne store with one consistency model
A platform where recovery is a first-class authentication routeA platform where recovery is an exception handler
A consumer of a signed metadata snapshotA caller of a metadata service on the hot path
A gate that refuses a cutover without the coverage figureAn arbiter of when a tenant's password should go
An RP ID on a domain the tenant ownsA convenient trust anchor on the platform's domain
A system where revocation is a state changeA system where revocation is a deletion

The decisions that are the architecture

01Ceremony state ephemeral and pinned; credential state durable and replicated

Two kinds of state with nothing in common but the word store. One retry's worth of loss on one side, a locked-out human on the other, so each gets the guarantee it actually needs.

ADR-01

02A challenge is consumed in a store, not carried as a token

A signature proves issuance; only a record proves use. A stateless challenge needs a nonce table to be safe, which is the same state with worse ergonomics.

ADR-02

03Tenant policy is read from memory, never from the control plane

Configuration read a billion times and written ten times belongs next to the reader. It is also the only way logins survive a total management-plane outage.

ADR-03

04Authenticator trust is admitted once and recorded immutably

Attestation is not resent with an assertion, so evaluating it per login means putting an external feed on the hot path and making the same credential's fate depend on publication timing.

ADR-04

05The metadata snapshot is signature-verified and fails static

Tolerate unavailable generously; refuse untrusted absolutely. An unsigned fresher copy is strictly worse than a signed older one.

ADR-05

06The sign counter is recorded and alerted on, enforced only by choice

Synced passkeys legitimately report zero. Strict enforcement does catch clones, and locks out far more people whose authenticator is working as designed.

ADR-06

07Fail closed on identity, open on advice, static on reference data

Three different questions deserve three different answers. A platform that gives all three the same answer is wrong twice, and the wrong answer was chosen by whoever wrote the catch block.

ADR-07

08Shed challenge issuance before assertion verification

A user who has already touched their sensor has done work the platform paid for. Refusing that is both the most wasteful shed and the most confusing failure.

ADR-08

09In-flight ceremonies are abandoned on failover, never migrated

Replicating disposable state makes single-use consumption probabilistic during exactly the conditions nobody can reason about. Price the retry instead.

ADR-09

10Every recovery route is classified and must end in a credential

An account protected by a passkey and recoverable by an emailed link is an account protected by an emailed link. The platform's job is to label that and carry the label forward.

ADR-10

11A recovery-enrolled credential waits before it may revoke

Recovery and revocation compose into an eviction attack. The attacker needs it immediate; the honest owner almost never does.

ADR-11

12The platform issues verdicts; the relying party issues sessions

Producing evidence is cheaper to be wrong about, and far cheaper to be breached with, than producing authority.

ADR-12

13Revocation is a state change; a policy change is never retroactive

A statement about the future and an act on the present need different blast radii. Share a mechanism and the quieter one eventually does the louder one's damage.

ADR-13

14One tenant-owned RP ID, with related origins for brands

Whoever owns the namespace a credential is scoped to decides whether the tenant can ever leave. A convenience RP ID on the platform's domain is a migration they cannot perform.

ADR-14

15The cutover is gated on measured two-or-more coverage

Below a threshold, switching the password off is a support incident with a security press release. Do not block the decision; block it being made without the number.

ADR-15

Why this should still be right in ten years

The store, the mesh, the proxy, the HSM and probably WebAuthn's own successor will all be replaced inside this platform's life. These five properties are the ones that should survive the replacement, and each is a statement about where something lives rather than about what it is built from.

State is separated by lifetime, not by subject

A record whose loss costs a retry and a record whose loss costs an account do not belong in the same store under the same guarantee, whatever the data model suggests. The value of that separation grows with scale rather than shrinking, because the cost of over-guaranteeing the disposable half is paid on every single operation.

The expensive judgement happens once and leaves an artefact

Registration resolves authenticator trust against external evidence and records what it decided, against what, and how stale that evidence was. Authentication checks the artefact. The same separation underlies signed build attestations, credit decisions and visas, and it survives every change of metadata format and authenticator technology.

Each dependency's failure direction is written down

Classifying dependencies by whether their absence makes the answer wrong or merely less informed is independent of what the dependencies are. The alternative is not an absence of policy; it is a policy chosen by a timeout default, which is how an advisory component takes a product down.

The platform produces evidence, not authority

A short-lived verifiable statement about what happened is a fundamentally smaller thing to breach than the power to act. The token format will change several times; the distinction between a notary and a bank will not.

The weakest entry path is measured and labelled

Strengthening the front door while leaving the recovery path unlabelled relocates the security rather than raising it. Carrying the route's strength into the decision that consumes it is the only mechanism that lets anyone downstream act on the difference, and it will matter for as long as humans lose devices.

Non-functional targets

Every figure below is a stated assumption. They are listed with the mechanism that is supposed to deliver them and the view where that mechanism is drawn, so a reviewer can disagree with the number and the means separately.

QualityTargetHow it is metView
Authentication availability ≥ 99.99% monthly Stateless verification replicas across three zones, region-pinned ceremony state, no control-plane dependency on the hot path 16
Management plane availability ≥ 99.9% monthly Deliberately lower: a console outage must not be able to affect logins, and is not treated as one 08
Assertion verification latency p50 ≤ 15 ms, p99 ≤ 60 ms One atomic local consume, one local credential read, policy from replica memory, signing off the critical section 13
Challenge issuance latency p99 ≤ 25 ms One strongly consistent single-key write to the region-pinned ceremony store 13
Registration finalisation latency p99 ≤ 120 ms Attestation verification against an in-process snapshot, plus a cross-region quorum write for the credential 14
Steady throughput 20,000 ceremonies/s per region 24 verifier and 12 issuer replicas per zone, horizontally scaled with no ceremony affinity 16
Burst 5× for 120 s Phase-ordered shedding at the edge: challenge issuance refused before assertion verification 18
Credential volume 400 M records, ~480 GB Regional-by-row partitioning by tenant and user handle; growth served by adding partitions 11
Ceremony state TTL 300 s, ~6 M live at peak In-memory, TTL-reclaimed, no cleanup job and no backup 11
Audit volume and retention 2.5 B events/yr; 13 months hot, 7 years cold Buffered stream into a columnar store, aged into hash-chained archive segments 10
RPO — credential records 0 Required, not assumed: synchronous quorum across three zones. A lost row is a locked-out human 16
RPO — audit stream ≤ 5 s Buffered emission that never backpressures a ceremony 10
RTO — regional failover ≤ 60 s Ingress withdrawal and capacity ramp only; no ceremony-state recovery step exists 16
Global credential convergence p99 ≤ 2 s Read-your-write in the registering region, bounded replication elsewhere, documented in the API contract 11
Platform-attributable ceremony failures ≤ 0.05% Measured on ceremony outcome codes, excluding user abandonment and authenticator-side cancellation 18
Recovery completion ≥ 95% within 10 min Three self-service routes with per-route cool-off; the operator-attested route is excluded from this measure 15
Verdict lifetime 60 s, single use, one audience HSM-signed, nonce-bound, audience-bound to one RP ID, verified offline by the relying party 21

Scope

In scope

  • Relying-party onboarding: tenants, RP IDs with verified domain control, explicit origin allowlists, and a staging environment whose credentials cannot authenticate against production
  • Versioned per-tenant authentication policy — user verification, attestation conveyance, resident-key requirement, authenticator attachment, counter mode, ceremony timeout — with pre-flight impact analysis and forward-only application
  • Registration ceremonies with excludeCredentials, attestation verification against a signed snapshot, and an immutable admission verdict written atomically with the credential
  • Authentication ceremonies for both discoverable and identifier-first flows, with single-use challenge consumption and full verification of client data, origin, RP ID hash and flags
  • Credential lifecycle: a human-readable device list, labelling, revocation as a state change, the last-credential guard, bulk revocation under second-approver control, and re-evaluation of stored verdicts against metadata compromise notices
  • Account recovery across four routes with assurance classification carried into the verdict, mandatory credential enrolment, per-route cool-off windows and operator attribution
  • Risk signals attached to the verdict, enumeration-resistant identifier-first responses, and phase-ordered load shedding
  • Migration from passwords: enrolment offers after a legacy login, three coverage figures per tenant, and a cutover gated on a tenant-acknowledged threshold
  • Signed verdict issuance and published verification material, with sessions remaining the relying party's responsibility
  • Append-only, hash-chained audit of every ceremony outcome, lifecycle action, recovery attempt and policy change, with tenant-pullable export

Explicitly out of scope

  • Authorization, entitlements and role management — the service authenticates; the relying party authorizes
  • Password storage and verification, which stays with the relying party for the migration window and is then removed by them
  • Session issuance, session lifetime, logout, session revocation and step-up decisions
  • Enterprise directory synchronisation and user provisioning
  • The relying party's own sign-in user interface and copy, beyond the ceremony SDK
  • Fraud and abuse decisioning beyond the signals attached to a verdict

What a four-week prototype should prove

The prototype's job is to falsify the three claims the whole design rests on — that single-use consumption really is cheap enough to sit on the hot path at rate, that a regional failure really does cost only a retry, and that recovery really can end in a working credential without the support desk becoming the authenticator. Everything else in the package can be adjusted; if any of those three is wrong, the boundary in ADR-01 has to move.

  1. One tenant with two RP IDs and a related-origin declaration, so both the single-brand and multi-brand origin paths are exercised rather than assumed
  2. Two regions with independent ceremony stores and one credential cluster with regional-by-row placement, because the failover claim cannot be tested in one region
  3. A mixed authenticator population: synced platform passkeys reporting a zero counter and device-bound security keys reporting a real one, so all three counter modes meet real inputs
  4. Three recovery routes wired end to end, including the one-time code and an out-of-band channel, with the cool-off window enforced rather than configured
  5. Fault injection for each declared degradation: credential store, ceremony store, risk engine and metadata snapshot, each failed independently
  6. An HSM or software PKCS#11 token in the verdict-signing path, so signing latency is measured rather than modelled
  • Two concurrent finish calls for one challenge are served by different replicas in the same region: exactly one succeeds, and the loser receives a replay rejection rather than a generic error, at 20,000 ceremonies per second
  • A region is killed mid-ceremony under load: in-flight clients receive the restart-the-ceremony error and complete on the second attempt, the surviving region absorbs both the new load and the retry burst, and no challenge is consumed twice
  • A credential is registered in one region and a login is attempted in the other within 100 ms: it fails, the failure is the documented convergence error rather than an invalid-credential error, and it succeeds within the 2-second target
  • The metadata snapshot fetch fails for seven consecutive days: registrations requiring attestation continue, every resulting verdict records a growing snapshot age, and an unsigned substitute is refused at every attempt including under a forced retry
  • The risk engine is slowed past its 15 ms deadline: ceremonies complete, every verdict carries unevaluated rather than a clean score, and an SDK that ignores the field is shown to produce a silently weaker decision
  • A recovery is completed by a second party who immediately attempts to revoke the owner's credential: the revocation is refused for the configured window, the owner is notified, and the owner's own credential retains full rights throughout
  • A policy tightening that would lock out 8% of a tenant's population is submitted: the impact-analysis gate refuses it, the override requires a named actor, and the attributed override appears in the audit trail within one second

Open risks, carried rather than hidden

RiskIf it landsResponse
The ceremony store becomes the platform's single point of failure in each region ADR-07 fails logins closed when it is unreachable, so a Redis incident is a regional authentication outage with no graceful degradation available Treat it as a tier-0 component with its own capacity headroom, zone spread and failover rehearsal; keep the restart-the-ceremony error path exercised so the failure is a retry storm rather than a support storm
Re-evaluation of stored admission verdicts produces a queue nobody works The window ADR-04 and ADR-05 deliberately accept never closes, and the platform's attestation story becomes a registration-time formality with no follow-through Make the raised-credential count a reported signal with an owner and an age distribution, not a work list; a non-zero count older than a threshold is an alert rather than a backlog
Relying parties ignore the assurance level and the unevaluated risk field in the verdict ADR-10 and ADR-07 both become decorative: a weak recovery grants the same access as a hardware passkey, and a risk engine outage silently weakens every decision Make both fields mandatory in the SDK's typed result rather than optional reads, publish per-tenant conformance reporting, and treat a tenant that never varies behaviour on assurance as an integration defect to raise
Tenants set meaningless cutover thresholds and clear their own gate ADR-15's control becomes theatre, the password is switched off at low coverage anyway, and the recovery path absorbs a load it was not sized for Report the threshold alongside the coverage and the resulting recovery volume per tenant, so the next cutover conversation starts from the last one's measured cost rather than from its announcement
The cooling-off window is routinely overridden by support under pressure ADR-11's protection evaporates exactly in the cases it exists for, since an attacker's best move is to call support and claim urgency Make the override a named, two-approver action with its own audit event and a monthly count; an override rate above a threshold is a process failure to investigate rather than a configuration to loosen
A single-credential population never becomes a two-credential population Every lost device becomes a recovery, recovery cost dominates the platform's marginal economics, and the cutover in ADR-15 is never honestly reachable Treat two-or-more coverage as the platform's primary product metric rather than enrolment count, and make the second-route prompt at registration a measured conversion funnel with its own owner

Passkey Authentication Service

Why every component and every technology on these 21 views is what it is, and what each choice costs.

Fifteen decisions, grouped by the question they answer, each with the forcing question, the alternatives, what would flip the choice, and why it should outlast the technology it is realised on.

Status of this document. This is a design, not a report on a running system. Every rate, latency, ratio, window and retention figure is a stated assumption, chosen to be defensible and arguable rather than measured. The operating context assumed throughout is a multi-tenant passkey platform serving 40 relying-party tenants and 180 million end-user accounts, averaging 2.3 credentials per user, at 20,000 authentication ceremonies per second per region with a 5× burst for 120 seconds, holding 400 million credential records in roughly 480 GB, emitting 2.5 billion authentication events a year, and running on self-hosted Kubernetes in two owned regions. Where the WebAuthn specification settles a question, the specification is followed and the decision records say so rather than claiming credit for it; every record here is about something the specification deliberately leaves to the relying party.

How to read a record

QuestionThe forcing question: why a decision was needed at all.
ContextThe requirement, the scale and the constraint that make it hard.
DecisionWhat this architecture does, stated so it can be checked.
How it is realised on the stackThe concrete mechanism: which component or package, configured how, in which cluster.
Options weighedChosen, rejected, deferred, or right elsewhere, with the reason for each.
ConsequencesWhat the choice buys and what it costs, both kept visible.
Choose differently whenThe conditions that would flip the decision for your system.
Why it holds up over timeWhat keeps the decision right as scale, staff and technology change.
LessonThe principle that transfers beyond this platform.

Decision map

The ceremony boundary 3

The decision that defines the architecture, and the two that follow from it directly.

ADR-01Ceremony state is ephemeral and region-pinned; credential state is durable and globally replicated ADR-02A challenge is consumed in a store, not carried as a stateless signed token ADR-03Tenant policy is read from an in-replica cache, never from the control plane, on the hot path

Trust and admission 3

When an authenticator is trusted, what is written down about it, and what is never re-decided on the login path.

ADR-04Authenticator trust is admitted once at registration and recorded as an immutable verdict ADR-05The authenticator metadata snapshot is signature-verified and fails static ADR-06The sign counter is recorded and alerted on by default, and enforced only where a tenant chooses

Degradation 3

What fails closed, what fails open, what fails static — and what a user sees in each case.

ADR-07Fail closed on identity, fail open on advice, fail static on reference data ADR-08Shed challenge issuance before assertion verification ADR-09In-flight ceremonies are abandoned on regional failover, never migrated

Recovery 2

The route back in, which sets the real assurance ceiling of every passwordless account.

ADR-10Every recovery route is classified by assurance and must end in a new credential ADR-11A credential enrolled through recovery waits before it may revoke anything

Boundaries and tenancy 4

Where this platform stops, how a tenant is isolated, and what actually ends the password.

ADR-12The platform issues signed verdicts; the relying party issues sessions ADR-13Revocation is a state change, and a policy change is never retroactive ADR-14One RP ID per tenant, with related-origin requests for multi-brand estates ADR-15The password cutover is gated on measured two-or-more-credential coverage

Technology by capability

The stack is open source, self-hosted on Kubernetes in two owned regions, chosen because no cloud genuinely owns WebAuthn: the specification is the interoperability layer, the libraries are mature and portable, and the parts of this platform that are hard — single-use ceremony state, per-row residency pinning, key custody that can be shown to an auditor — are hard in the same way on every provider. Self-hosting also forces the custody question to be answered rather than named: a managed key service would let the design say "keys are managed" where this one has to say which HSM, reachable from where, signing what. Every row below names what would be used instead on a managed platform, because the requirement is vendor-neutral and a reviewer is entitled to ask what the self-hosted choice costs.

Open source This design
CapabilityChoiceOriginCredible alternativeWhy this oneRecord
Ceremony state: single-use challenges, region-pinned, 300-second TTL Redis Cluster, one cluster per region, no cross-region replication, consumption by Lua script open source A managed in-memory store per region, with cross-region replication explicitly disabled Single-key atomicity on a shard owner is exactly the guarantee single-use consumption needs, and nothing more; the absence of a replication link is a configuration that cannot be got wrong ADR-02
Credential store: durable, globally replicated, per-row residency CockroachDB with regional-by-row tables, leaseholders following the tenant's pin open source core A managed globally distributed SQL database with per-row region placement Residency becomes a row property rather than a deployment per tenant, which keeps the per-tenant cost floor low while satisfying the strictest tenant ADR-01
Key custody: verdict and session signing Network HSM pair per region, PKCS#11, with Vault sealed against it and holding no exportable key open source plus hardware A managed key service with an HSM-backed tier and no export Sign-only with no export path is a property an auditor can verify physically; a standing HSM pair per region is the accepted cost floor of saying it ADR-12
Edge: TLS termination, origin checking, phase-aware shedding Envoy as the ceremony gateway, with per-phase concurrency budgets open source A managed global edge with a WAF and per-route rate limits Shedding has to understand which half of the ceremony a request is, which needs a configurable proxy rather than a generic rate limit ADR-08
Service-to-service identity and encryption Linkerd mesh with mTLS and per-workload identity open source A managed mesh or the platform's own workload identity Every inter-service hop is authenticated by workload rather than by network position, which is what makes the zone model in view 20 more than a drawing ADR-12
Evidence: append-only audit, 13 months hot, 7 years cold Kafka into ClickHouse, sealed hash-chained segments into MinIO open source A managed event stream into a columnar warehouse, with object-store archive and object lock Buffered emission keeps the audit pipeline off the ceremony's latency budget, and hash chaining makes a gap detectable rather than deniable ADR-13
Policy and configuration distribution Versioned documents streamed to in-replica caches, published by the policy service built A managed configuration service with push and per-client versioning The hot path must not read the control plane, and tightening a security control needs an acknowledgement step no off-the-shelf config service offers ADR-03
Delivery: code and tenant policy to production Argo CD per region with digest-addressed signed images and OPA Gatekeeper admission open source A managed GitOps or deployment service with image attestation policy Policy changes travel the same gated pipeline as code because both can lock a user out, and only one of them was previously reviewed ADR-11
Observability: metrics, traces, logs and SLO alerting Prometheus and Grafana, OpenTelemetry tracing, Loki for logs open source A managed observability suite with the same signal types The signals that matter here are unusual — ceremony outcome rate, configuration age, hash-chain continuity — and all three need custom instrumentation rather than a provider's defaults ADR-07
Authenticator metadata ingest CronJob pulling the FIDO MDS BLOB, signature-verified against a Vault-held trust anchor open source plus a public feed The same job on a managed scheduler, with the anchor in a managed key service The valuable part is not the fetch, it is the refusal: there is no code path that accepts an unverified snapshot, however fresh ADR-05

The decisions, and the alternatives that lost

The ceremony boundaryThe decision that defines the architecture, and the two that follow from it directly.

ADR-01

Ceremony state is ephemeral and region-pinned; credential state is durable and globally replicated

Accepted

A login touches two kinds of state with nothing in common but the word "store". Do they get one storage strategy or two, and if two, where exactly is the line?

Context
A WebAuthn ceremony needs a challenge that is written, read once, and thrown away inside five minutes, and it needs the credential's public key, which must outlive every component in the system. Treating them the same way forces a choice that is wrong for one of them: a single globally consistent store puts cross-region consensus on the latency path of every login, and a single eventually consistent store cannot guarantee that a challenge is consumed exactly once, which is the one correctness property the ceremony actually depends on. The two also fail differently and must be allowed to. Losing a challenge costs the user a retry — annoying, recoverable, and visible only as a restarted login. Losing a credential record locks a human being out of their account permanently, because in a passwordless design there is nothing else to fall back on. A storage decision that treats a five-minute token and an irreplaceable public key as the same class of data will end up either too slow or too lossy, and in both directions the damage is paid by the user.
Decision
Ceremony state lives in a region-pinned, strongly consistent, short-TTL store and is never replicated across regions. Credential state lives in a globally replicated, read-mostly store with read-your-write in the registering region and bounded convergence elsewhere. The authentication hot path mutates the first, reads the second, and touches no other durable store.
How it is realised on the stack
Per region, a Redis Cluster holds ceremony records keyed by ceremony id with a 300-second TTL; each record is owned by exactly one shard, which is what makes single-use consumption an atomic single-key operation rather than a distributed agreement. Nothing replicates between regions' Redis clusters — the configuration deliberately has no cross-region link to misconfigure. Credential records live in one CockroachDB cluster spanning both regions with regional-by-row tables: a tenant's residency pin sets the row's region, the leaseholder follows it, and reads in that region are local. Registration writes pay cross-region quorum latency; authentication reads do not. Verification replicas carry no state at all and are scheduled freely within a region.
Options weighed
  • ChosenTwo stores, split by lifetime and consistency need: Each kind of state gets the guarantee it actually needs; costs a retry for in-flight users during a regional failure and a bounded convergence window after registration
  • RejectedOne globally consistent store for both: Puts cross-region consensus inside every login's latency budget to buy a property that single-key atomicity already provides within one region
  • RejectedOne eventually consistent store for both: Cannot guarantee a challenge is consumed exactly once, which is the ceremony's only real correctness requirement
  • RejectedCeremony state in the credential store, as short-lived rows: Makes a 20,000-per-second write rate the credential store's problem, and couples the availability of the irreplaceable data to the churn of the disposable data
Consequences
What it buys
  • The latency target is reachable with no distributed transaction anywhere in a login: one local atomic operation and one local read
  • A regional failure costs in-flight users a retry and can never produce a wrong decision in a surviving region, because there is no stale replica of ceremony state to read
  • The backup budget collapses onto one store: ceremony state is not backed up at all, which is correct rather than negligent
  • Residency is a row-level property of the credential store rather than a separate deployment per tenant, which keeps the per-tenant cost floor low
What it costs
  • Ceremony state cannot survive a regional failure, so a small population of users must restart a login during one
  • A registration followed within milliseconds by a login in another region can fail; the convergence window is bounded at 2 seconds and has to be documented in the API contract
  • Two storage technologies to operate, patch and capacity-plan instead of one
  • The region pin is now load-bearing for correctness as well as compliance, so a routing mistake has a correctness consequence and not just a latency one
Choose differently when
If every tenant were single-region and the whole user base could be served from one site, the split would buy much less — one store with the stronger guarantee would be simpler and fast enough, and the retry-on-failover cost would disappear. The decision is justified by a multi-region estate with per-tenant residency obligations and a latency target that cannot absorb cross-region agreement.
Why it holds up over time
"Separate state by its lifetime, not by its subject" outlives every product named here. The specific technologies are replaceable — a different consensus store, a different KV, a different consistency vocabulary — but the boundary between data whose loss costs a retry and data whose loss costs an account survives all of them. What would not survive is reuniting them: the moment the irreplaceable data shares an availability fate with the disposable data, the system's worst failure mode becomes its most frequent one.
LessonAsk what losing each store costs the user. Two answers that far apart are two stores, whatever the data model says.
Shown on views02 08 11 16
ADR-02

A challenge is consumed in a store, not carried as a stateless signed token

Accepted

The challenge could be a signed, encrypted token the client carries, with no server state at all. What does keeping server state actually buy, and is it worth the store?

Context
The attraction of a stateless challenge is real: no store on the hot path, no TTL to tune, nothing to fail over, and a ceremony that works in any region that holds the signing key. WebAuthn does not require the relying party to keep server-side state, and plenty of implementations do not. The problem is the word "consumed". A signed token proves the platform issued the challenge; it cannot prove the platform has not already accepted an assertion for it. Replay protection therefore needs somewhere to record that this challenge has been used — a nonce table, a consumed-token set, a bloom filter — at which point the state is back, just less honest about it, and now split between a token and a side table whose consistency nobody reasons about. A short TTL narrows but does not close the window, and the window is exactly where a stolen assertion is replayed. There is a second, quieter cost: a stateless challenge cannot carry the policy version it was issued under without being trusted to carry it, which means a policy change mid-ceremony can move the rules under a user whose authenticator is already prompting.
Decision
The challenge is a server-side record with a TTL, consumed exactly once by an atomic operation. A replayed challenge is rejected even when the signature over it is perfectly valid. The record also pins the policy version in force when the ceremony began.
How it is realised on the stack
Beginning a ceremony writes one Redis key holding the challenge bytes, the tenant, the user handle where the flow is identifier-first, the policy version id and an expiry. Finishing it runs a Lua script that reads, checks and deletes the key in one shard-local operation, so two concurrent finishes cannot both succeed regardless of which replica serves them. The 300-second TTL means expired records cost nothing to collect. Verification reads the policy version from the ceremony record, not from the live config cache, so a policy published mid-ceremony cannot change the rules a prompting authenticator is being judged against.
Options weighed
  • ChosenServer-side record, atomically consumed: Replay prevention is unconditional and cheap; costs one region-pinned store on the hot path and a restart-the-ceremony failure mode
  • RejectedStateless signed and encrypted challenge token: Cannot be consumed without some server record, so the state reappears as a nonce table with weaker guarantees and worse ergonomics
  • RejectedStateless token plus a short-TTL replay cache, best effort: Trades an absolute property for a probabilistic one in exchange for operational simplicity the platform does not need
  • RejectedChallenge derived from a per-user counter: Makes concurrent ceremonies on two devices interfere, and puts a write on the credential store for every login attempt
Consequences
What it buys
  • Replay is impossible rather than unlikely, and the property holds without reference to clock skew or token lifetime
  • The policy version is pinned per ceremony, so a mid-ceremony policy publication cannot change a user's outcome
  • Single-use consumption is one atomic shard-local operation, which is why the store can sustain the ceremony rate
  • Abandoned ceremonies are reclaimed by TTL expiry and never need a cleanup job
What it costs
  • A store on the hot path, with its own availability, capacity and failover story
  • A ceremony-store outage fails logins closed; the error taxonomy must distinguish restart-the-ceremony from stop-trying or support cannot triage it
  • Roughly 6 million live records at peak to hold in memory, which is a sizing commitment
  • A ceremony cannot be finished in a region other than the one that began it, which constrains routing
Choose differently when
For a single-tenant relying party with modest traffic and no replay concern beyond the authenticator's own protections — an internal tool behind a VPN, say — a stateless challenge would be the right call, and the store would be pure overhead. The decision is justified by a multi-tenant platform where replay protection is a property being sold.
Why it holds up over time
"Single-use means something records the use" is a statement about where a guarantee lives, and it will outlive Redis, WebAuthn and this platform. Any nonce, any idempotency key, any one-time token faces the identical choice, and the stateless version always looks cheaper until the consumption requirement is written down. What would not survive is pretending a signature is a consumption record.
LessonA signature proves issuance. Only a record proves use. If a design needs the second, it has state whether or not it admits it.
Shown on views08 11 13
ADR-03

Tenant policy is read from an in-replica cache, never from the control plane, on the hot path

Accepted

Every ceremony needs the tenant's current policy. Is that a call to the control plane, a shared cache, or a copy inside each verification replica?

Context
A verification replica needs the tenant's RP IDs, origin allowlist, user-verification requirement, attestation mode and counter policy before it can judge a single assertion. A call to the control plane per ceremony is the obvious implementation and the one that quietly makes the control plane a tier-0 dependency: at 20,000 ceremonies per second it also becomes the busiest service in the platform, serving data that changes a few times a month. A shared cache tier improves the arithmetic but keeps a network hop and adds a component whose failure is indistinguishable from the control plane's. The deeper problem with both is availability coupling. The requirement is explicit that the authentication path stays serviceable while the management plane is entirely down, and a design where a login reads from the control plane cannot honour that no matter how many caches are placed in front of it. Against that, a replica-local copy is stale by construction, and staleness in an origin allowlist is a security-relevant property, not just an operational one.
Decision
Each verification replica holds its own copy of tenant configuration, refreshed asynchronously, and reads it from memory. Configuration reads never appear on the hot path as a network call. Policy publication is a push with a bounded convergence target, and a ceremony's own policy version is pinned on its ceremony record.
How it is realised on the stack
The policy service writes each tenant's configuration as a versioned document; verification replicas subscribe at start-up and then stream updates, holding the current and previous versions in memory. A replica that has lost its update stream keeps serving its last good copy and reports its configuration age as a metric, which is alertable. Origin allowlist tightening is handled as a two-phase publication: the new, narrower list is distributed and acknowledged by every replica before it becomes effective, so no replica is ever the only one still accepting a withdrawn origin. Loosening needs no such ceremony. The impact-analysis gate in the release pipeline runs against the published configuration before any of this starts.
Options weighed
  • ChosenReplica-local copy, asynchronously refreshed: Removes the control plane from the hot path entirely and survives its total loss; costs a bounded staleness window and a two-phase publication for tightening changes
  • RejectedControl-plane call per ceremony: Makes the coldest service in the platform a tier-0 dependency of every login, and violates the stated requirement that logins survive a management-plane outage
  • RejectedShared cache tier in front of the control plane: Keeps a network hop and adds a component whose failure looks identical to the thing it was protecting
  • RejectedConfiguration baked into the replica image: Turns every tenant policy change into a deployment, which is both slower than the tenants need and a far larger blast radius
Consequences
What it buys
  • Policy evaluation costs a memory read, which is what makes the 15 ms p50 budget realistic
  • The authentication path survives a complete control-plane outage, as the requirement demands
  • Configuration age is a first-class metric, so staleness is observable rather than assumed away
  • A replica that cannot reach the control plane degrades to serving correctly rather than failing
What it costs
  • Every policy change has a convergence window, and that window is security-relevant for origin tightening
  • A two-phase publication for tightening changes is extra machinery that only one class of change needs
  • Each replica holds the whole tenant estate's configuration, which bounds how many tenants a replica can serve before memory matters
  • Debugging a per-tenant behaviour difference now means asking which replica served the request and how old its copy was
Choose differently when
With a handful of tenants and a policy that changes once a year, a direct control-plane read with a short cache would be simpler and the availability argument would be weaker, because the control plane could be made as available as the ceremony plane. The decision is justified by 40 tenants with independently changing policy and an explicit requirement that logins outlive the management plane.
Why it holds up over time
"Push reference data to where it is read; never pull it on a latency path" is older than any technology in this package and will outlast all of them. The mechanism changes — a sidecar, a stream, a mounted volume, a service mesh's own config plane — but the property that matters is that the reader's correctness does not depend on the writer's availability at read time. What would not survive is a design where tightening a security control is also asynchronous without an acknowledgement step.
LessonConfiguration that is read a billion times and written ten times belongs in memory next to the reader, and the only hard part is the change that must not be late.
Shown on views07 08 17

Trust and admissionWhen an authenticator is trusted, what is written down about it, and what is never re-decided on the login path.

ADR-04

Authenticator trust is admitted once at registration and recorded as an immutable verdict

Accepted

Attestation says which authenticator model created a credential. Is that evaluated once, at registration, or on every authentication?

Context
The temptation is to evaluate policy on every assertion, because that is where the risk is and because policy can change. It does not work well. Attestation material is produced at registration and is not resent with an assertion, so authentication-time evaluation means looking the credential's model up in a metadata snapshot on every login — putting an external reference feed on the hot path and making a login's outcome depend on how fresh that feed is. It also makes authentication non-deterministic in an uncomfortable way: the same credential and the same signature can be accepted at 09:00 and refused at 09:05 because a metadata publication landed between them, which from the user's seat is indistinguishable from a broken product. The alternative is to treat admission as a decision made once, with evidence, and written down: the resolved model, the attestation result, the policy version that admitted it, and how stale the metadata snapshot was at the time. Authentication then verifies cryptography against a recorded decision, which is both faster and far easier to reason about — and the question of what to do about a model later found compromised becomes a separate, deliberate re-evaluation rather than an accident of timing.
Decision
Registration resolves and records an immutable admission verdict per credential. The authentication path verifies the signature, origin, RP ID hash and flags against the stored credential and its verdict, and never re-evaluates authenticator policy or consults the metadata service. Changes in metadata or policy are handled by an explicit re-evaluation job that raises affected credentials for a decision.
How it is realised on the stack
The admission verdict is a separate one-to-one row keyed by credential id, holding the AAGUID, the attestation outcome, the policy version id that admitted it and the age in seconds of the metadata snapshot used. It is written in the same transaction as the credential row and has no update path in the application: changing an admission decision means revoking the credential, not editing the verdict. A nightly job re-reads the current snapshot against stored verdicts and writes a raised-for-decision list, which appears as a count on the lifecycle dashboard; nothing in that job revokes anything.
Options weighed
  • ChosenAdmit once at registration, record an immutable verdict: Keeps the metadata feed off the hot path and makes authentication deterministic; costs a window where a later-revoked model still authenticates
  • RejectedEvaluate authenticator policy on every assertion: Puts an external feed on the login path and makes the same credential's outcome depend on publication timing
  • RejectedRe-evaluate on a schedule and auto-revoke: Hands an upstream publisher the ability to log out a tenant's entire user base; see ADR-13 on why revocation is never automatic
  • RejectedNo attestation at all; treat every authenticator alike: Defensible for consumer tenants and offered as a policy option, but it cannot be the platform default when workforce tenants must enforce a model allowlist
Consequences
What it buys
  • Authentication is pure verification against a stored decision, which is both the fast path and the auditable one
  • The metadata service is not a login dependency, so its outage is a registration-quality question rather than an availability incident
  • The same assertion gets the same answer today and tomorrow, which is what makes support triage possible
  • Snapshot staleness is recorded per credential, so the population admitted during a stale window is findable rather than theoretical
What it costs
  • A model revoked upstream today can still authenticate today; the window is real and is closed by a job, not by the hot path
  • A second table and a second write in the registration transaction
  • Re-evaluation produces a queue that needs a human, and an unattended queue is this decision's realistic failure mode
  • Policy tightening does not improve the existing population's posture until somebody acts on the raised list
Choose differently when
In an environment where a compromised authenticator model is an active, present threat and the user base is small enough to re-enrol — a few thousand workforce users with managed security keys — authentication-time evaluation with automatic refusal would be the right trade, because the cost of a stale admission is higher than the cost of a sudden lockout. The decision is justified by a 180-million-account consumer and workforce estate where sudden mass refusal is the larger harm.
Why it holds up over time
"Decide once, record the decision and the evidence, verify against the record" is the same separation as a signed build attestation, a credit decision, or a visa: the expensive judgement happens once and leaves an artefact, and the frequent operation checks the artefact. It survives every change of metadata format and every change of authenticator technology. What would not survive is letting the verification path quietly start making judgements again, because then there are two policies and only one of them is written down.
LessonPut the slow, external, judgement-heavy work at the rare event and leave the frequent event a lookup. Then write down what was judged, and when, and against what.
Shown on views12 14 19
ADR-05

The authenticator metadata snapshot is signature-verified and fails static

Accepted

The metadata feed is an external dependency that can be unavailable, stale, or tampered with. Which of those is allowed to stop a registration?

Context
Authenticator metadata is reference data published by someone else, and a platform that consumes it inherits three distinct problems that are easy to conflate. Unavailability is routine and uninteresting: a feed that cannot be fetched today does not make yesterday's copy wrong. Staleness is the real cost of tolerating unavailability, and it is bounded and measurable. Tampering is the one that matters, and it is the one a naive implementation handles worst — an unsigned or unverified update is strictly more dangerous than no update, because it can add an attacker's AAGUID to an allowlist and every credential admitted under it will carry a verdict saying trust was verified. The failure mode to avoid, then, is not a stale snapshot; it is a platform that prefers a fresher unsigned copy to an older signed one because freshness was the thing it was built to optimise.
Decision
The metadata snapshot is fetched periodically, its signature is verified against a pinned trust anchor, and an update that fails verification is discarded with an alert. When no valid newer snapshot is available the platform fails static on the last good one, and records the snapshot's age on every admission verdict it supports.
How it is realised on the stack
A Kubernetes CronJob pulls the FIDO Metadata Service BLOB daily, verifies its signature chain against a trust anchor held in Vault, and on success publishes it as a new immutable snapshot that attestation verifiers load. A verification failure leaves the previous snapshot in place, raises an alert and does not retry with relaxed checking — there is no relaxed path in the code. Attestation verifiers read only the published snapshot and never the network. Each admission verdict records the snapshot age in seconds, so the population admitted during any stale window can be enumerated later with a query rather than reconstructed from logs.
Options weighed
  • ChosenVerify signature, fail static on the last good snapshot: Tampering is refused absolutely while unavailability is absorbed; costs a bounded trust-staleness window that is recorded per credential
  • RejectedFail closed: refuse attestation-requiring registrations when the feed is stale: Converts a routine upstream outage into a registration outage for exactly the tenants with the strictest policy, which is the wrong direction
  • RejectedFetch live on each registration: Puts a third-party endpoint on the registration path and makes an upstream outage a platform outage
  • RejectedAccept unsigned updates when the signature cannot be checked: Strictly worse than no update: it lets an attacker's model be admitted under a verdict that claims trust was verified
Consequences
What it buys
  • An upstream outage has no effect on registrations, which is the correct response to a reference feed being briefly unreachable
  • Tampering is refused by construction rather than by vigilance, because no code path accepts an unverified snapshot
  • Trust staleness is a recorded, queryable property of each credential instead of an unknown
  • The hot path never touches the feed at all, which follows from ADR-04 and is preserved here
What it costs
  • A model revoked upstream remains admissible until the next successful fetch, and the re-evaluation job is what closes the gap
  • The trust anchor is now a key with a rotation obligation and a compromise story of its own
  • A silently failing CronJob degrades invisibly unless snapshot age is alerted on, which makes that alert load-bearing
  • Operators may be tempted to force an unsigned update during an upstream incident; the absence of that path is a deliberate inconvenience
Choose differently when
If attestation were being used to enforce a hardware allowlist in a high-assurance environment where admitting a compromised model is unacceptable at any staleness, fail-closed would be right: better to stop registrations than to admit one wrong authenticator. The decision is justified by a platform where most tenants request attestation for assurance reporting rather than for strict enforcement.
Why it holds up over time
"Prefer old and verified to new and unverified" is the rule for every reference feed, revocation list, root store and rule set a system has ever consumed, and it is nearly always violated under incident pressure. The format and the publisher will change; the ordering of those two preferences should not. What would not survive is an emergency override that accepts an unsigned update, because the first time it is used it becomes the path.
LessonSeparate unavailable from untrusted. Tolerate the first generously, refuse the second absolutely, and measure how long you have been tolerating.
Shown on views11 14 20
ADR-06

The sign counter is recorded and alerted on by default, and enforced only where a tenant chooses

Accepted

WebAuthn's signature counter exists to detect a cloned authenticator. With synced passkeys reporting zero, is enforcing it still clone detection or just a lockout generator?

Context
The counter was designed for a world of single-device hardware authenticators, where a counter that goes backwards is strong evidence that the credential has been copied. That world is now a minority of the population. Synced passkeys legitimately report a counter of zero, because a credential that exists on several devices cannot maintain a meaningful global counter; some platform authenticators report inconsistent values across devices by design. Strict enforcement in that environment does detect genuine clones, and it also locks out users whose authenticator is behaving exactly as its vendor intended — and the second group is far larger than the first. Ignoring the counter entirely discards the one clone signal the protocol offers, which is unacceptable for a workforce tenant with hardware keys and a real cloning threat model. The honest position is that this is a tenant-specific threat-model decision rather than a platform-wide correctness one, and that the platform's job is to make the choice explicit, to make the default safe for the common case, and to never change which mode is in force without saying so.
Decision
Counter handling is per-tenant policy with three modes: enforce monotonicity and reject regressions, record-and-alert, or ignore. Record-and-alert is the default. The mode in force is recorded on the credential's policy version and in the audit trail, and it is never changed silently.
How it is realised on the stack
The credential row carries the last observed counter and the backup-eligible flag; the tenant's policy version carries the counter mode. On every assertion the verifier compares the presented counter to the stored value and takes the policy's action: reject with a distinct error code, emit a counter-regression audit event and continue, or skip the comparison. The counter update is written asynchronously, so counter handling never appears in the latency budget. Because backup-eligible is stored alongside, the support desk and the alerting pipeline can tell a synced credential's zero counter apart from a device-bound credential's genuine regression, which is the distinction that makes the alert worth reading.
Options weighed
  • ChosenPer-tenant mode, record-and-alert by default: Preserves the signal for tenants that can act on it without making the common case a lockout; costs a configuration surface and an alert nobody may read
  • RejectedEnforce monotonicity platform-wide: Locks out users whose synced authenticator is behaving as designed, at a rate that dwarfs the clone detections it buys
  • RejectedIgnore the counter platform-wide: Discards the protocol's only clone signal, which workforce tenants with hardware keys legitimately need
  • RejectedInfer the mode from the backup-eligible flag automatically: Attractive and nearly right, but it makes the security posture depend on a flag the authenticator reports, and silently changes enforcement when a vendor changes behaviour
Consequences
What it buys
  • Users with synced passkeys are not locked out by a mechanism that does not apply to them
  • Tenants with a genuine cloning threat model can still enforce, and their choice is visible in the audit trail
  • Storing backup-eligible makes the alert interpretable, which is the difference between a signal and noise
  • Counter handling is off the latency path entirely
What it costs
  • Three modes is three code paths to test, and the rejecting path is the least exercised and most dangerous
  • The default produces alerts that may go unread, which is a weaker posture than it appears on paper
  • A tenant switching to enforce mode can lock out an existing population, so the change has to pass the impact-analysis gate like any other
  • Support must be able to explain why one tenant's regression is a block and another's is a note
Choose differently when
If the authenticator population were entirely device-bound hardware — a workforce deployment with issued security keys and no platform passkeys — enforcement would be the obvious default and the per-tenant surface would be unnecessary complexity. The decision is justified by a mixed population dominated by synced credentials.
Why it holds up over time
"A signal that was strong under one deployment assumption becomes a liability when the assumption changes" is the general shape here, and it recurs wherever a protocol outlives the hardware it was designed for. The counter will eventually matter less, not more. What survives is the discipline of making the enforcement decision explicit and attributable instead of letting an implementation default become an unexamined security claim.
LessonWhen a protocol's own anti-fraud signal stops matching the deployed population, make the enforcement a stated choice with a safe default — not a code-level default nobody remembers making.
Shown on views12 13 19

DegradationWhat fails closed, what fails open, what fails static — and what a user sees in each case.

ADR-07

Fail closed on identity, fail open on advice, fail static on reference data

Accepted

Five dependencies can be unavailable during a login. Which of them is allowed to let the login through, and which must stop it?

Context
Most availability incidents in an authentication platform are not the authentication path failing; they are something beside it failing and nobody having decided in advance what that means. Left undecided, the answer is whatever the error handler's author happened to write, which means the platform's security posture under stress is set by a timeout default. The dependencies divide cleanly once the question is asked properly — not "is this component important" but "if this component cannot answer, is the login still correct?" The credential store answers whether a credential may authenticate at all, so a login without it is not a degraded decision, it is a guess. The risk engine contributes advice that sharpens a decision the platform can already make, so a login without it is correct but less informed. The metadata snapshot is reference data whose last known value is nearly always still right. Three different questions, three different answers, and a platform that gives all three the same answer is wrong twice.
Decision
Credential-store unavailability fails closed: the platform does not authenticate from cache when it cannot confirm a credential's current state. Risk-engine unavailability fails open with the verdict marked unevaluated. Metadata unavailability fails static on the last signed snapshot. Ceremony-store unavailability fails closed with a distinct restart-the-ceremony error. Recovery-channel failure degrades to the tenant's remaining configured routes. Every one of these is a declared behaviour with a test.
How it is realised on the stack
Each dependency call carries its own deadline and its own declared failure action in code, and the verdict structure has an explicit field for every signal that could be absent — there is no sentinel value that could be mistaken for a negative result. The risk engine's 15 ms deadline expiring produces the string "unevaluated" in the verdict, which relying parties are contractually required to handle and which the SDK surfaces rather than swallows. The restart-the-ceremony error is its own error code in the API, distinct from both an invalid assertion and a generic server failure, so a client can retry correctly and a support agent can tell the three apart. A fault-injection suite exercises each declared behaviour in staging on every release.
Options weighed
  • ChosenPer-dependency declared failure direction: Each degradation is a decision with a test; costs a larger error taxonomy and a contract relying parties must honour
  • RejectedFail closed on everything: Makes the risk engine and the metadata feed into tier-0 login dependencies, which is how an advisory component takes down a product
  • RejectedFail open on everything: Authenticating without confirming a credential's revocation state is not degradation, it is a security failure
  • RejectedA global circuit breaker with one policy: Uniform behaviour across dependencies with different meanings; the uniformity is the defect, not the feature
Consequences
What it buys
  • An advisory component's outage costs information, not availability — which is what makes it safe to have one
  • A credential-store outage cannot authenticate anybody incorrectly, because there is no cache path that could
  • The error taxonomy lets a client retry correctly and lets support triage from the error code alone
  • Each behaviour is tested by fault injection, so the declared posture and the actual posture stay the same thing
What it costs
  • A credential-store outage is a full authentication outage for the affected region, with no graceful degradation available
  • Relying parties must handle an unevaluated risk field, which is a contract obligation some will ignore
  • More error codes means more to document and more for an SDK to get wrong
  • Fault-injection coverage has to be maintained or the declarations quietly become aspirations
Choose differently when
A platform whose availability requirement genuinely outranked its correctness requirement — an internal tool where being locked out is worse than a stale authorisation — might choose to serve from a credential cache during an outage with a tightly bounded staleness. That is a defensible design; it is not this one, and the difference is a stated priority rather than a technical disagreement.
Why it holds up over time
"Classify dependencies by whether their absence makes the answer wrong or merely less informed" is the durable idea, and it applies to feature flags, fraud scoring, entitlement caches and licence checks alike. Technology churn does not touch it. What would not survive is an undeclared timeout: the moment a dependency's failure direction is implicit, it has been chosen by whoever wrote the catch block.
LessonFor every dependency on a critical path, write down what happens when it does not answer. The ones you do not write down are still decided — just not by you.
Shown on views13 18 20
ADR-08

Shed challenge issuance before assertion verification

Accepted

Under a burst beyond provisioned capacity, something has to be refused. Which half of a two-call ceremony is the one to drop?

Context
A ceremony is two calls: begin, which issues a challenge, and finish, which verifies the assertion. Under overload the naive response is to shed uniformly, which sounds fair and is close to the worst available option. A user whose begin call succeeded has already been prompted by their authenticator and has already touched a sensor or entered a PIN; refusing their finish call wastes work the platform has already paid for, wastes an interaction the user has already performed, and produces the most confusing possible failure — a login that asked for a fingerprint and then said no. Refusing a begin call, by contrast, is a clean and early refusal: nothing has been promised, nothing is in flight, and the client can back off and retry with a message that makes sense. Uniform shedding also has a perverse dynamic under sustained load, because every shed finish call tends to produce a fresh begin call as the user retries, so the system converts load into more load.
Decision
Load shedding is ordered: challenge issuance is refused before assertion verification, and both are refused before the management plane is affected. The order is declared, implemented at the edge, and reported as a metric per class.
How it is realised on the stack
The edge gateway classifies requests by ceremony phase and applies separate concurrency limits with separate queue budgets, sized so that the finish-call budget always exceeds the begin-call admission rate by the expected completion ratio. Shed begin calls return a retry-after and a distinct error code; the SDK surfaces it as a transient condition rather than an authentication failure. Shed rate is a metric per class and per tenant, and the SLO alert fires on begin-shed above 1% for five minutes — not on total request failures, which would hide the distinction this decision exists to make.
Options weighed
  • ChosenOrdered shedding: begin before finish: Protects work already paid for and produces a comprehensible refusal; costs a phase-aware edge and two budgets to size
  • RejectedUniform shedding across both calls: Wastes completed authenticator interactions and converts load into retries, which is load amplification under exactly the wrong conditions
  • RejectedQueue everything, shed nothing: Trades refusals for latency until the queue outlives the ceremony TTL, at which point the work is wasted anyway and later
  • RejectedShed by tenant tier: Orthogonal and also desirable, but it answers who to protect rather than what to protect, and it does not prevent wasting in-flight work
Consequences
What it buys
  • A user who has already touched their sensor is very unlikely to be refused, which is where the user-visible damage of overload concentrates
  • Overload produces a retryable, intelligible refusal early instead of a confusing one late
  • The retry-amplification loop is broken, so a burst does not feed itself
  • Shed rate per class is directly observable, making capacity decisions concrete
What it costs
  • The edge has to understand the ceremony protocol, so a protocol change can require an edge change
  • Two budgets to size and keep in a correct ratio as the completion rate drifts
  • Under extreme load new logins stop entirely while in-flight ones succeed, which looks like a total outage to anyone who was not already mid-ceremony
  • A client that retries aggressively on a shed begin can still amplify, so the retry-after has to be honoured by the SDK
Choose differently when
For a single-call authentication protocol the decision does not arise, and for a platform with enough headroom that shedding never happens it is unexercised machinery. The decision is justified by a 5× burst target over provisioned steady-state capacity, which is a deliberate choice not to provision for the peak.
Why it holds up over time
"Protect work in progress ahead of work not yet started" is the same instinct as admission control in a database, a restaurant refusing bookings rather than abandoning seated diners, and a scheduler finishing a job before accepting the next. It long predates WebAuthn and will outlast it. What would not survive is a load-shedding implementation that cannot distinguish the phases, because then fairness is the only policy available.
LessonUnder overload, refuse at the beginning. The cheapest refusal is the one made before anybody has done anything.
Shown on views02 18 20
ADR-09

In-flight ceremonies are abandoned on regional failover, never migrated

Accepted

A region fails with six million live ceremony records. Is any attempt made to let those users finish, or are they all asked to start again?

Context
Ceremony state is region-pinned by ADR-01, which means a regional failure takes with it every challenge issued in that region but not yet consumed. The instinct is to soften that: replicate the ceremony store asynchronously, or hand the surviving region a signed summary, so that a user mid-login does not notice. Both ideas reintroduce the problem ADR-01 exists to avoid. An asynchronously replicated challenge can be present in two regions with different consumed flags, which makes single-use consumption probabilistic exactly when the system is least able to reason about it — during a failover, with partitioned components and operators under pressure. A signed hand-off summary is worse, because it is a stateless challenge token with extra steps, and ADR-02 already explains why that cannot be consumed safely. The cost of abandoning instead is small and bounded: a five-minute window of in-flight logins, which at peak is a large number of users and a trivial amount of work, each of whom sees one retryable error.
Decision
Ceremony state is never replicated, in any direction, synchronously or otherwise. On regional failover, in-flight ceremonies in the lost region are abandoned and their clients receive the restart-the-ceremony error. The surviving region serves new ceremonies only.
How it is realised on the stack
The two regions' Redis clusters have no replication configuration between them, so there is nothing to misconfigure under pressure and nothing for an operator to enable during an incident. Anycast ingress withdraws the failed region and new ceremonies land on the survivor. A finish call arriving for a ceremony id the surviving region has never seen returns the distinct restart-the-ceremony error code, which the SDK handles by beginning again rather than surfacing an authentication failure — so the user sees a repeated prompt, not a refusal. RTO for the authentication path is 60 seconds, which is the ingress withdrawal and capacity ramp, not a state recovery.
Options weighed
  • ChosenNever replicate; abandon in-flight ceremonies: Single-use consumption stays absolute and failover stays simple; costs a five-minute window of users retrying one login
  • RejectedAsynchronously replicate ceremony state: A challenge present in two regions with divergent consumed flags makes replay protection probabilistic during exactly the worst conditions
  • RejectedSynchronously replicate ceremony state: Puts cross-region consensus into every ceremony begin, which is the latency cost ADR-01 was taken to avoid
  • RejectedSigned hand-off summary to the surviving region: A stateless challenge token wearing a different name; ADR-02 applies to it unchanged
Consequences
What it buys
  • Replay protection is unconditional, including during a failover, which is when an attacker would most like it not to be
  • Failover is an ingress and capacity operation with no state recovery step, which is why 60 seconds is achievable
  • There is no replication link to misconfigure, and no emergency toggle for an operator to reach for
  • Capacity planning per region is independent, because neither region carries the other's ceremony state
What it costs
  • Every user mid-ceremony in the failed region retries, which at peak is a visible spike of restarted logins
  • The restart error must be handled by every client correctly, or those users see an authentication failure instead of a retry
  • The surviving region absorbs both regions' new ceremony load plus a retry burst, which sets its headroom requirement
  • A user who abandoned a login during the failover and returns later finds nothing preserved, which is correct and still feels like a fault
Choose differently when
If a ceremony were long-lived — a multi-step enrolment taking minutes of user effort that could not be repeated — abandoning it would be unacceptable and some form of durable, strongly consistent cross-region state would have to be paid for. The decision is justified by a ceremony that costs the user one authenticator prompt to repeat.
Why it holds up over time
"Do not replicate state whose loss costs a retry" is the general rule, and its value grows with scale rather than shrinking: the bigger the system, the more the replication of disposable state costs and the less it buys. What would not survive is an operator-facing switch to enable replication during an incident, because the one time it is used will be the one time nobody can reason about the consequences.
LessonBefore replicating state, price the retry. Replication is often more expensive than the thing it prevents, and always more expensive to reason about.
Shown on views11 16 20

RecoveryThe route back in, which sets the real assurance ceiling of every passwordless account.

ADR-10

Every recovery route is classified by assurance and must end in a new credential

Accepted

With no password to reset, recovery is the only way back into a locked-out account. What is it allowed to prove, and when is it finished?

Context
Passkeys remove the shared secret, and with it the familiar recovery path. What replaces it is whatever the tenant configures, and that choice silently sets the account's real security level: an account protected by a hardware-backed passkey and recoverable by an emailed link is, to an attacker, an account protected by an emailed link. The common failure is not that this is unknown but that it is unrecorded — recovery happens outside the authentication model, issues a session indistinguishable from a strong one, and the relying party never learns which path the user came in by. The second failure is subtler and more expensive: a recovery that returns a session without enrolling a credential leaves the user in exactly the state that caused the call, so the next login is another recovery, and the support cost recurs forever. Both are fixed by treating recovery as a first-class authentication route with a declared strength and a required outcome, rather than as an exception handler.
Decision
Every tenant must configure at least one recovery route, and the platform refuses production traffic for one that has none. Each route carries an assurance classification which is carried into the verdict, so a tenant can permit sign-in but forbid high-value actions from a weak route. A recovery that does not end in an enrolled credential is recorded as incomplete, not successful.
How it is realised on the stack
Four routes are supported — a second enrolled passkey, a one-time code issued at enrolment, a tenant-configured out-of-band channel, and delegated verification by the tenant's support process — each with a platform assurance level of high, medium, low or operator-attested. The recovery orchestrator writes a recovery_attempt row carrying the method, the assurance level, the operator id where a human was involved, the outcome and the credential id it enrolled; a null credential id is what makes an incomplete recovery queryable. Attempts are rate-limited per handle and per source network, and a burst across many handles raises a platform abuse signal rather than many independent per-user events. The verdict's assurance field is contractual: relying parties are required to read it, and the SDK surfaces it.
Options weighed
  • ChosenClassified routes, mandatory credential enrolment: The account's real assurance becomes visible and actionable; costs a configuration obligation on tenants and a contract obligation on relying parties
  • RejectedRecovery as an unclassified side path: Issues a session indistinguishable from a strong one, so the weakest route silently sets every account's security level
  • RejectedMandate a second passkey at enrolment and support nothing else: The strongest design and commercially unacceptable: the enrolment friction loses the users the migration exists to convert
  • RejectedLeave recovery entirely to the tenant: Makes the platform's phishing-resistance claim unfalsifiable, since the real authentication path would be outside it
Consequences
What it buys
  • A tenant can allow a weak recovery to sign in without allowing it to move money, which is the distinction that makes weak routes tolerable at all
  • Incomplete recoveries are a queryable number rather than a recurring support mystery
  • Cross-account recovery bursts are visible as one abuse signal instead of many unrelated per-user events
  • Refusing to serve a tenant with no configured route removes the worst configuration from the estate by construction
What it costs
  • Four routes is four flows to build, test and support, and the operator-attested one involves human process the platform cannot verify
  • The assurance field is only as useful as relying parties' willingness to read it, and some will not
  • Mandatory enrolment at the end of recovery adds a step to the worst moment in the product
  • A tenant whose only route is an out-of-band channel still has a low ceiling; the platform can report that but not fix it
Choose differently when
In a workforce setting with a staffed identity desk and in-person verification available, operator-attested recovery would be the primary route and the others would be conveniences, because the human check is genuinely strong there. The decision is justified by a consumer-scale estate where most recoveries must complete without a human.
Why it holds up over time
"A system's real security is the strength of its weakest entry path, so measure and label every path" is as old as locksmithing and gets rediscovered every time an authentication factor is strengthened without touching the reset flow. What survives is carrying the path's strength forward into the decision that consumes it. What would not survive is a recovery flow that issues the same token as a strong authentication, because then nobody downstream can act on the difference.
LessonStrengthening the front door while leaving the back door unlabelled does not raise security; it relocates it. Label the back door and carry the label forward.
Shown on views05 15 12
ADR-11

A credential enrolled through recovery waits before it may revoke anything

Accepted

An attacker who completes a recovery can enrol their own passkey and immediately revoke the owner's. What stops the legitimate owner being evicted from their own account?

Context
Recovery and revocation compose badly. Each is reasonable alone: recovery gets a locked-out user back in, and revocation lets a user remove a credential they no longer control. Together they hand whoever completes a recovery first the ability to remove everyone else — which in an account takeover means the attacker locks out the owner using the owner's own security feature, and the owner's remaining credential becomes worthless. The asymmetry that makes this fixable is time. An attacker needs the eviction to be immediate, because the owner will notice and contest it; the legitimate owner, who has just been through recovery because they lost a device, almost never needs to revoke the lost credential in the same minute. A delay therefore costs the honest user patience and costs the attacker the attack. The uncomfortable part is that the legitimate owner's wait is real: someone who has genuinely lost their phone cannot immediately evict it, and that will be the single most frustrating moment in the product.
Decision
A credential enrolled at the end of a recovery cannot authorise a revocation until a tenant-configurable cooling-off window has elapsed. The window scales inversely with the recovery route's assurance: none for a second enrolled passkey, and up to 24 hours for an operator-attested recovery. The owner's existing credentials keep full rights throughout.
How it is realised on the stack
The recovery_attempt row records which credential it enrolled; the revocation service checks that credential's enrolment provenance and the elapsed time against the tenant's policy before accepting a revocation request. A second enrolled passkey proves possession of something the attacker does not have, so its window is zero. During the window the account is fully usable for authentication — the restriction is narrowly on revoking other credentials, not on signing in — and the owner is notified on every channel the tenant has configured, which is what gives them the chance to contest. A credential outside any window is unaffected, so the normal add-a-device flow carries no delay.
Options weighed
  • ChosenAssurance-scaled cooling-off on revocation rights only: Removes the immediate-eviction attack while keeping the account usable; costs the legitimate owner a wait at the worst possible moment
  • RejectedNo restriction: Lets a successful takeover evict the owner using the owner's own security feature, which makes recovery a net security loss
  • RejectedRequire two existing credentials to revoke: Impossible for the single-credential user, who is precisely the population that needs recovery
  • RejectedHuman review of every post-recovery revocation: Correct and unscalable; it also puts the support desk back in the position of being the real authenticator
Consequences
What it buys
  • An attacker who completes a recovery cannot evict the owner before the owner can contest it
  • The notification window gives the owner a concrete action, which is what makes the delay protective rather than merely obstructive
  • A second enrolled passkey — the route the platform wants users to take — carries no delay at all, which aligns the incentive
  • The restriction is narrow: the account signs in normally throughout, so the cost is bounded to one capability
What it costs
  • A user who has genuinely lost a device cannot evict it immediately, and will reasonably describe that as the product working against them
  • Another tenant-configurable window to explain, tune and support
  • An attacker who waits out the window still succeeds; this buys detection time, not immunity
  • Support will be asked to override it, and every override is a hole in the control that has to be attributed
Choose differently when
If the platform could notify a user through a channel an attacker provably does not control, the window could shrink towards zero because contesting would be near-instant. It cannot, which is the honest reason the delay exists: the window is a substitute for a trustworthy out-of-band channel.
Why it holds up over time
"When two legitimate capabilities compose into an attack, separate them in time and notify" is the shape of a bank's new-payee delay, a domain registrar's transfer lock and a password manager's account-recovery hold. It will outlive passkeys. What would not survive is applying the delay to authentication rather than to the destructive capability, because that punishes the honest user for the attacker's option.
LessonWhen you cannot tell two actors apart, stop trying and make the attacker's move require patience the honest user can afford to give.
Shown on views05 15 19

Boundaries and tenancyWhere this platform stops, how a tenant is isolated, and what actually ends the password.

ADR-12

The platform issues signed verdicts; the relying party issues sessions

Accepted

After a successful ceremony the user needs a session. Does this platform become the tenant's identity provider, or hand back a statement and stop?

Context
Becoming the identity provider is the commercially obvious move and the architecturally expensive one. Issuing sessions means owning session lifetime, logout, revocation, step-up, device binding and every integration with the tenant's existing authorisation model — and it means that a compromise of this platform is a compromise of every tenant's live sessions rather than of their login path. It also means replacing infrastructure the tenant already has and already trusts, which is the single largest adoption barrier for a platform whose whole proposition is an incremental upgrade to an existing login. The alternative is to return a signed statement about what happened, verifiable offline, and let the tenant mint whatever they already mint. The cost is that the statement has to be good: it must be short-lived, single-use, audience-bound and must carry everything the tenant needs to make its own step-up decision, because the platform will not be consulted again. It also means the platform cannot answer "log this user out", which some tenants will ask for.
Decision
A successful ceremony returns a signed verdict: short-lived, single-use, bound to one RP ID, carrying the credential used, the user-verification state, the assurance level of the path taken and the risk signals that applied. The relying party verifies it offline against published material and issues its own session. Logout, session lifetime, revocation and step-up stay with the relying party.
How it is realised on the stack
The verdict is a compact signed token with a 60-second lifetime, a single-use nonce, an audience of exactly one RP ID, and explicit fields for every signal — including absent ones, so an unevaluated risk result cannot be mistaken for a clean one. Signing keys live in the HSM behind Vault; verification material is published for relying parties to cache, which bounds key rotation effect to the cache lifetime. Platform administrators authenticate through this same flow against the platform's own RP ID, so the platform is its own first relying party and the flow is exercised by the people who would notice it being wrong.
Options weighed
  • ChosenSigned verdict, relying party issues the session: Bounds the consequence of a platform compromise and removes the largest adoption barrier; costs the platform any ability to revoke a session
  • RejectedFull identity provider issuing sessions and tokens: Makes a platform compromise a compromise of every tenant's live sessions, and requires replacing session infrastructure tenants already trust
  • RejectedBoth, tenant's choice: Two security models, two blast radii and two sets of revocation semantics to reason about; the flexibility is bought with permanent ambiguity
  • RejectedOpaque receipt the relying party must call back to redeem: Puts this platform on the critical path of every session establishment, so its outage becomes the tenant's outage
Consequences
What it buys
  • A compromise of this platform compromises the login path, not every tenant's live sessions
  • Adoption does not require replacing session infrastructure, which is what makes an incremental migration possible at all
  • Offline verification means a platform outage does not prevent a relying party validating a verdict it already holds
  • The platform being its own first relying party means the flow is exercised daily by people who would notice it breaking
What it costs
  • The platform cannot answer "log this user out", and some tenants will treat that as a missing feature rather than a boundary
  • The verdict's contents are a contract: adding a field is easy, and changing the meaning of one is not
  • A relying party that skips the nonce, audience or assurance checks turns a correct verdict into a replayable token, and nothing in the platform can prevent that
  • Key rotation takes effect only as fast as relying parties' caches expire
Choose differently when
For a tenant with no session infrastructure of its own — a greenfield product that would otherwise build one — being the identity provider is genuinely the better offer, and a separate product could reasonably do it. The decision is justified by an estate of 40 relying parties that all already issue sessions and would all have to be rebuilt.
Why it holds up over time
"Return a verifiable statement, not an entitlement" is the distinction between a notary and a bank, and between an attestation and a capability. It survives every token format. What would not survive is a verdict that is accepted as a session by convention: the moment a relying party stops minting its own and starts forwarding the platform's, the boundary is gone whatever the documentation says.
LessonDecide whether you are producing evidence or producing authority. Evidence is cheaper to be wrong about, and far cheaper to be breached with.
Shown on views01 09 21
ADR-13

Revocation is a state change, and a policy change is never retroactive

Accepted

Two operations can take a credential out of service: revoking it, and changing the policy that admitted it. Should they behave the same way?

Context
They look similar and must not be. Revocation is a deliberate act about one credential or an enumerated set, usually in response to a loss or a compromise, and it needs to be immediate, attributable and auditable. A policy change is a statement about what will be admitted in future, and tenants make them routinely — tightening user verification, narrowing an authenticator allowlist, changing counter mode. If a policy change applied retroactively, every routine tightening would silently invalidate some existing population, and a tenant adjusting a setting on a Tuesday afternoon would discover on Wednesday morning that they had locked out forty thousand users. Conflating the two also corrupts the audit trail: a credential that stopped working needs to show whether somebody revoked it or whether a configuration edit made it inadmissible, and those are different incidents with different owners. The third trap is deletion. Deleting a credential row looks like the cleanest possible revocation and destroys the evidence of why it happened, which is exactly what an attacker with access to the management plane would want.
Decision
Revocation is a state change on a retained record, never a deletion, with a reason and an actor. A policy change applies to new ceremonies only and can never invalidate an existing credential; taking an existing population out of service requires an explicit bulk revocation with a second approver and a reported affected-user count. Policy changes pass an impact-analysis gate before they apply.
How it is realised on the stack
The credential row carries a state of active or revoked with a reason and an actor, and there is no delete path in the application — erasure deletes at the request of the user, which is a different operation with a different authorisation. Policy versions carry an effective-from timestamp and are read from a ceremony's own pinned version, so a change cannot reach a ceremony already in flight. The release pipeline's impact-analysis gate evaluates a proposed policy against the tenant's existing credential population and refuses the change above a lockout threshold; an override is explicit and attributed. Bulk revocation by authenticator model or policy version is a separate command requiring two approvers and reporting the affected count before it executes.
Options weighed
  • ChosenRevocation as state change; policy forward-only with an impact gate: Routine tightening is safe and destructive action is deliberate; costs a two-approver flow and an analysis gate to build and maintain
  • RejectedPolicy changes apply retroactively: Makes every routine configuration edit a potential mass-lockout event with no review step in front of it
  • RejectedRevocation by deleting the credential row: Destroys the evidence of why a credential stopped working, which is precisely what a management-plane compromise would want to do
  • RejectedSoft-delete with a retention job that eventually purges: Reintroduces deletion on a timer, so the audit trail's lifetime is set by a cleanup policy rather than by the retention requirement
Consequences
What it buys
  • Tenants can tighten policy without fear, which means they actually do it
  • A credential that stopped working can always be explained: who revoked it, when and why, or that it was never revoked at all
  • The two most destructive operations in the platform both require a second approver and both report their blast radius first
  • The audit trail outlives the credential, which is what makes a post-incident reconstruction possible
What it costs
  • Revoked records accumulate and are retained for seven years, which is storage spent on things that will never authenticate again
  • A tenant wanting an immediate estate-wide change must go through bulk revocation rather than a policy edit, which is slower on purpose
  • The impact-analysis gate needs a current view of the credential population, so it is a real piece of machinery and not a lint check
  • Two-approver flows are routinely resented and routinely worked around; the attribution is what makes the workaround visible
Choose differently when
In a small single-tenant deployment where every policy change is already reviewed by the person who will handle the support calls, retroactive application with an immediate re-enrolment push would be simpler and the gate unnecessary ceremony. The decision is justified by 40 tenants editing their own policy without visibility of each other's populations.
Why it holds up over time
"Separate the statement about the future from the act on the present, and never destroy the record of the act" is the same discipline as an append-only ledger, a migration versus a backfill, and a law versus a judgement. It outlasts every schema. What would not survive is a delete path added for convenience, because the first legitimate use of it establishes that revocation can be invisible.
LessonA configuration change and an operational act need different blast radii and different approvals. If one mechanism does both, the quieter one will eventually do the louder one's damage.
Shown on views12 17 19
ADR-14

One RP ID per tenant, with related-origin requests for multi-brand estates

Accepted

A tenant with six consumer brands on six domains wants one passkey to work across all of them. Where does the RP ID boundary go?

Context
The RP ID is the domain a credential is scoped to, and it decides something the tenant experiences as a product question: does a passkey enrolled on one brand work on another. Three answers are available and each sells a different future. An RP ID per brand gives clean separation and means a user enrols separately on each brand, which for a retail group with a shared account is experienced as the product being broken. One RP ID per tenant, with the other brands' origins declared as related origins, gives one credential across the estate at the cost of a declared trust relationship between those domains — and at the cost that divesting a brand means the divested domain can no longer use credentials it has been relying on. The third option is for the platform to offer its own domain as a shared RP ID, which is operationally simplest, works everywhere immediately, and makes the platform's domain the trust anchor for every tenant's authentication. That last point is a lock-in no serious tenant should accept, and a platform that offers it as a convenience will find it has become the default.
Decision
Each tenant gets one RP ID on a domain it controls and has proved control of, with additional brand origins declared as related origins. The platform does not offer its own domain as an RP ID. A tenant that genuinely needs separate credential populations per brand may have multiple RP IDs, declared as such.
How it is realised on the stack
Domain control is verified before an RP ID is activated, and the origin allowlist is explicit per RP ID; a ceremony whose client data origin is not on the list is rejected server-side regardless of the request's own headers. Related-origin support is published per RP ID as the specification requires, and tightening an origin list uses the two-phase acknowledged publication from ADR-03 so no replica is left as the only one still accepting a withdrawn origin. A tenant with multiple RP IDs sees them as separate credential populations in the console, because they are, and the migration-coverage figures are reported per RP ID rather than summed.
Options weighed
  • ChosenOne tenant-owned RP ID, related origins for brands: One credential across the estate on a domain the tenant owns; costs a declared trust relationship between brand domains and a divestment problem
  • RejectedRP ID per brand: Clean separation, and users must enrol separately on each brand, which a shared-account estate experiences as the feature not working
  • RejectedPlatform's own domain as a shared RP ID: Operationally simplest and makes this platform the trust anchor for every tenant's authentication, which is a lock-in a tenant should refuse
  • RejectedLet each tenant choose freely with no default: The choice is irreversible for enrolled credentials, so a platform that offers it without a recommendation is handing over a decision most tenants will make once, quickly, and regret
Consequences
What it buys
  • A credential works across the tenant's brands without the tenant depending on this platform's domain
  • Domain verification before activation stops a tenant claiming a domain it does not own
  • Credential populations and coverage are reported per RP ID, so a multi-RP-ID tenant sees the truth rather than a flattering sum
  • An exit from this platform does not require users to re-enrol, because the RP ID belongs to the tenant
What it costs
  • Divesting a brand strands credentials for that domain, and there is no migration path the specification allows
  • Related origins are a trust declaration between domains, so a compromise of one brand's origin affects the estate's credentials
  • A tenant restructuring its domains is doing something with authentication consequences, which it will not expect
  • The platform cannot offer the one-line integration a shared RP ID would allow, which is a real sales cost
Choose differently when
For a platform deliberately positioned as a consumer identity hub — where users expect one identity across unrelated sites and would see per-tenant credentials as a defect — a shared RP ID on the platform's domain is the product rather than a mistake. The decision is justified by a business-to-business platform whose tenants must be able to leave.
Why it holds up over time
"Identity namespaces should be owned by whoever must be able to leave" is the same argument as owning your email domain rather than using a provider's, and it long predates WebAuthn. What survives is the ownership question. What would not survive is a convenience RP ID on the platform's domain, because every tenant that takes it becomes unable to migrate, and that eventually becomes the platform's commercial strategy whether anybody chose it or not.
LessonAsk who owns the namespace a credential is scoped to. If the answer is you rather than your customer, you have sold them a migration they cannot perform.
Shown on views01 09 12
ADR-15

The password cutover is gated on measured two-or-more-credential coverage

Accepted

A tenant asks to switch the password off. What has to be true first, and who decides — the tenant, or the platform?

Context
Turning the password off is the only point at which a passkey deployment's phishing-resistance claim becomes true, and it is also the point at which every user holding exactly one credential becomes one lost device away from a recovery call. The two facts are usually discussed separately: the security claim at a board review, the support capacity in a different meeting. The result is a cutover announced on security grounds and absorbed by a support organisation that was not asked. What makes this tractable is that the relevant number is measurable and the platform already has it — the share of a tenant's active users holding two or more usable credentials. Below some threshold a cutover is not a security improvement, it is a support incident with a security press release. The platform cannot know a given tenant's tolerable lockout rate, which is a business judgement about their own users and their own support capacity, but it can refuse to let the decision be made without the number in front of whoever makes it.
Decision
Passkey-only cutover is refused while a tenant's two-or-more-credential coverage sits below a tenant-acknowledged threshold. The threshold is set by the tenant, not the platform. An override is explicit and attributed. Removing the legacy path is recorded as an irreversible configuration change, and recovery-path cost is reported as its own line so the cutover's real expense is visible.
How it is realised on the stack
Three coverage figures are computed nightly per tenant from the audit stream — any credential, two or more, and still legacy-dependent — and surfaced in the console and the migration report. The cutover request is a configuration change that passes the same impact-analysis gate as any policy change; below the acknowledged threshold it is refused, and the override requires a named actor and is written to the audit trail. The legacy-removal flag has no reverse transition in the state machine, so a tenant that switches the password off cannot quietly switch it back on. Recovery volume and cost per route are reported alongside coverage, because that is where the cutover's cost lands and it is the number that will be used to argue about the next one.
Options weighed
  • ChosenTenant-set threshold, platform-enforced gate, attributed override: The decision stays the tenant's and cannot be made blind; costs a refusal that will be read as the platform obstructing its own value proposition
  • RejectedNo gate: cutover is purely the tenant's call: Lets a security decision be taken without the support consequence being visible, which is how passwordless migrations acquire their reputation
  • RejectedA platform-set threshold: Substitutes the platform's judgement for the tenant's about their own users' tolerance and their own support capacity
  • RejectedAdvisory warning only: A warning in a console is read by the person clicking the button, which is rarely the person who will answer the calls
Consequences
What it buys
  • The cutover decision rests on three measured figures rather than on a sense that the rollout went well
  • The override exists, so the platform does not become something to be worked around, and it is attributed, so it becomes something to be argued about
  • Irreversibility makes the phishing-resistance claim real: a password that can be quietly re-enabled was never gone
  • Recovery cost reported per route gives the next cutover conversation a number instead of an anecdote
What it costs
  • The platform refuses the action a tenant most wants to take, at the moment of most enthusiasm, which reads as obstruction
  • Coverage is the slowest-moving signal in the package and will be ignored until a cutover is requested
  • A tenant can set a meaningless threshold and clear its own gate, which makes the control partly theatre
  • Irreversibility means a tenant that cuts over too early has only recovery to fall back on, and recovery will be loaded
Choose differently when
With a workforce tenant that can mandate enrolment of two credentials as a condition of access, the gate is unnecessary: coverage reaches the threshold by policy before anyone asks. The decision is justified by consumer tenants who can only offer, never require.
Why it holds up over time
"Gate an irreversible migration on a measured readiness figure, and make the override attributable" is the shape of every safe cutover — a database migration, a protocol deprecation, a certificate-algorithm sunset. It outlives passkeys entirely. What would not survive is a threshold the platform sets, because that is a judgement about someone else's users, and it will be wrong in both directions for different tenants.
LessonDo not block the decision; block the decision being made without the number. And make the override cost a name, not a click.
Shown on views06 17 18

Every package used, in one table

Ten terms used throughout the package in a specific sense. Where a term is from the WebAuthn specification the sense is the specification's; where it is this platform's own, the row says so.

PackageWhat it isWhat it does hereConsidered instead
Ceremony One registration or authentication exchange, from the challenge being issued to the assertion being verified or the attempt being abandoned. The unit of work, the unit of shedding, and the only thing on the hot path that owns mutable state. Often called a login attempt, which obscures that it is two calls with a user interaction between them.
Challenge At least 16 random bytes bound to one tenant, one user handle and one ceremony, consumed exactly once. The replay defence. Its single-use consumption is why the ceremony store exists. A nonce, in the general cryptographic sense.
User handle An opaque, non-reassignable identifier carrying no personal data, stable for the life of the account. What the authenticator stores and what the audit trail retains after an erasure request. The specification's user.id; not the relying party's own subject identifier, which is a separate column.
Admission verdict This platform's own term: the immutable record of what was decided about an authenticator at registration, and against what evidence. What the authentication path verifies against, instead of re-evaluating policy. The subject of ADR-04. No standard equivalent; the nearest analogue is a signed build attestation.
Attestation The authenticator's statement, at registration, about which model created the credential. The input to the admission verdict. Never resent with an assertion, which is why it cannot be evaluated at authentication time. The specification's attestation statement and conveyance preference.
Assertion The authenticator's signature over the client data and authenticator data in response to a challenge. What the hot path verifies: signature, origin, RP ID hash and flags against the stored credential. Loosely called a signed login response.
RP ID The domain a credential is scoped to, and the boundary across which a credential cannot be used. Decides whether a credential works across a tenant's brands. The subject of ADR-14. The specification's relying party identifier; related-origin requests widen the origin set without widening the RP ID.
Verdict This platform's output: a short-lived, single-use, audience-bound signed statement that a ceremony succeeded and under what conditions. What the relying party exchanges for its own session. The platform never issues a session. The subject of ADR-12. Similar in shape to an OIDC id_token, deliberately not one, because it conveys no session.
Assurance level This platform's classification of how strongly the route taken actually proved the user's identity. Carried into the verdict so a tenant can allow sign-in but forbid high-value actions after a weak recovery. The subject of ADR-10. Comparable to an authenticator assurance level, but about the route rather than the device.
Fail static Continuing to serve from the last known-good copy of reference data when no valid newer copy is available. The declared behaviour for authenticator metadata, as distinct from failing closed or failing open. The subject of ADR-05. Sometimes called stale-while-error or last-known-good.
Open svg/<view>.svg or drawio/<view>.drawio in draw.io Desktop or at app.diagrams.net to edit. The SVG carries the diagram inside it, so it is both the picture and the source. This folder is self-contained — copy it whole and every link still resolves.