Document 67 min read

Passkey Authentication Service

Solution Architecture v1.0 · open source, self-hosted on Kubernetes · Security & Identity Architecture · 2026-10

Passkey Authentication Service · Solution Architecture v1.0 · open source, self-hosted on Kubernetes · Security & Identity Architecture · 2026-10

The argument these decisions serve is summarised in the Architecture One-Pager.

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

  • Question: The forcing question: why a decision was needed at all.
  • Context: The requirement, the scale and the constraint that make it hard.
  • Decision: What this architecture does, stated so it can be checked.
  • How it is realised on the stack: The concrete mechanism: which component or package, configured how, in which cluster.
  • Options weighed: Chosen, rejected, deferred, or right elsewhere, with the reason for each.
  • Consequences: What the choice buys and what it costs, both kept visible.
  • Choose differently when: The conditions that would flip the decision for your system.
  • Why it holds up over time: What keeps the decision right as scale, staff and technology change.
  • Lesson: The principle that transfers beyond this platform.

Decision map

The ceremony boundary: The 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
  • ADR-02 · A challenge is consumed in a store, not carried as a stateless signed token
  • ADR-03 · Tenant policy is read from an in-replica cache, never from the control plane, on the hot path

Trust and admission: When 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
  • ADR-05 · The authenticator metadata snapshot is signature-verified and fails static
  • ADR-06 · The sign counter is recorded and alerted on by default, and enforced only where a tenant chooses

Degradation: What 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
  • ADR-08 · Shed challenge issuance before assertion verification
  • ADR-09 · In-flight ceremonies are abandoned on regional failover, never migrated

Recovery: The 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
  • ADR-11 · A credential enrolled through recovery waits before it may revoke anything

Boundaries and tenancy: Where 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
  • ADR-13 · Revocation is a state change, and a policy change is never retroactive
  • ADR-14 · One RP ID per tenant, with related-origin requests for multi-brand estates
  • ADR-15 · The 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.

Capability Choice Origin Credible alternative Why this one Record
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 boundary

The 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

Status: Accepted · Shown on views: 02, 08, 11, 16

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.

Option Verdict Reasoning
Two stores, split by lifetime and consistency need Chosen 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
One globally consistent store for both Rejected Puts cross-region consensus inside every login's latency budget to buy a property that single-key atomicity already provides within one region
One eventually consistent store for both Rejected Cannot guarantee a challenge is consumed exactly once, which is the ceremony's only real correctness requirement
Ceremony state in the credential store, as short-lived rows Rejected 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

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.

Lesson. Ask what losing each store costs the user. Two answers that far apart are two stores, whatever the data model says.

ADR-02 · A challenge is consumed in a store, not carried as a stateless signed token

Status: Accepted · Shown on views: 08, 11, 13

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.

Option Verdict Reasoning
Server-side record, atomically consumed Chosen Replay prevention is unconditional and cheap; costs one region-pinned store on the hot path and a restart-the-ceremony failure mode
Stateless signed and encrypted challenge token Rejected Cannot be consumed without some server record, so the state reappears as a nonce table with weaker guarantees and worse ergonomics
Stateless token plus a short-TTL replay cache, best effort Rejected Trades an absolute property for a probabilistic one in exchange for operational simplicity the platform does not need
Challenge derived from a per-user counter Rejected Makes concurrent ceremonies on two devices interfere, and puts a write on the credential store for every login attempt

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.

Lesson. A signature proves issuance. Only a record proves use. If a design needs the second, it has state whether or not it admits it.

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

Status: Accepted · Shown on views: 07, 08, 17

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.

Option Verdict Reasoning
Replica-local copy, asynchronously refreshed Chosen 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
Control-plane call per ceremony Rejected 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
Shared cache tier in front of the control plane Rejected Keeps a network hop and adds a component whose failure looks identical to the thing it was protecting
Configuration baked into the replica image Rejected Turns every tenant policy change into a deployment, which is both slower than the tenants need and a far larger blast radius

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.

Lesson. Configuration 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.

Trust and admission

When 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

Status: Accepted · Shown on views: 12, 14, 19

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.

Option Verdict Reasoning
Admit once at registration, record an immutable verdict Chosen Keeps the metadata feed off the hot path and makes authentication deterministic; costs a window where a later-revoked model still authenticates
Evaluate authenticator policy on every assertion Rejected Puts an external feed on the login path and makes the same credential's outcome depend on publication timing
Re-evaluate on a schedule and auto-revoke Rejected Hands an upstream publisher the ability to log out a tenant's entire user base; see ADR-13 on why revocation is never automatic
No attestation at all; treat every authenticator alike Rejected 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

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.

Lesson. Put 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.

ADR-05 · The authenticator metadata snapshot is signature-verified and fails static

Status: Accepted · Shown on views: 11, 14, 20

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.

Option Verdict Reasoning
Verify signature, fail static on the last good snapshot Chosen Tampering is refused absolutely while unavailability is absorbed; costs a bounded trust-staleness window that is recorded per credential
Fail closed: refuse attestation-requiring registrations when the feed is stale Rejected Converts a routine upstream outage into a registration outage for exactly the tenants with the strictest policy, which is the wrong direction
Fetch live on each registration Rejected Puts a third-party endpoint on the registration path and makes an upstream outage a platform outage
Accept unsigned updates when the signature cannot be checked Rejected Strictly worse than no update: it lets an attacker's model be admitted under a verdict that claims trust was verified

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.

Lesson. Separate unavailable from untrusted. Tolerate the first generously, refuse the second absolutely, and measure how long you have been tolerating.

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

Status: Accepted · Shown on views: 12, 13, 19

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.

Option Verdict Reasoning
Per-tenant mode, record-and-alert by default Chosen 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
Enforce monotonicity platform-wide Rejected Locks out users whose synced authenticator is behaving as designed, at a rate that dwarfs the clone detections it buys
Ignore the counter platform-wide Rejected Discards the protocol's only clone signal, which workforce tenants with hardware keys legitimately need
Infer the mode from the backup-eligible flag automatically Rejected 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

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.

Lesson. When 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.

Degradation

What 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

Status: Accepted · Shown on views: 13, 18, 20

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.

Option Verdict Reasoning
Per-dependency declared failure direction Chosen Each degradation is a decision with a test; costs a larger error taxonomy and a contract relying parties must honour
Fail closed on everything Rejected Makes the risk engine and the metadata feed into tier-0 login dependencies, which is how an advisory component takes down a product
Fail open on everything Rejected Authenticating without confirming a credential's revocation state is not degradation, it is a security failure
A global circuit breaker with one policy Rejected Uniform behaviour across dependencies with different meanings; the uniformity is the defect, not the feature

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.

Lesson. For 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.

ADR-08 · Shed challenge issuance before assertion verification

Status: Accepted · Shown on views: 02, 18, 20

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.

Option Verdict Reasoning
Ordered shedding: begin before finish Chosen Protects work already paid for and produces a comprehensible refusal; costs a phase-aware edge and two budgets to size
Uniform shedding across both calls Rejected Wastes completed authenticator interactions and converts load into retries, which is load amplification under exactly the wrong conditions
Queue everything, shed nothing Rejected Trades refusals for latency until the queue outlives the ceremony TTL, at which point the work is wasted anyway and later
Shed by tenant tier Rejected Orthogonal and also desirable, but it answers who to protect rather than what to protect, and it does not prevent wasting in-flight work

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.

Lesson. Under overload, refuse at the beginning. The cheapest refusal is the one made before anybody has done anything.

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

Status: Accepted · Shown on views: 11, 16, 20

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.

Option Verdict Reasoning
Never replicate; abandon in-flight ceremonies Chosen Single-use consumption stays absolute and failover stays simple; costs a five-minute window of users retrying one login
Asynchronously replicate ceremony state Rejected A challenge present in two regions with divergent consumed flags makes replay protection probabilistic during exactly the worst conditions
Synchronously replicate ceremony state Rejected Puts cross-region consensus into every ceremony begin, which is the latency cost ADR-01 was taken to avoid
Signed hand-off summary to the surviving region Rejected A stateless challenge token wearing a different name; ADR-02 applies to it unchanged

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.

Lesson. Before replicating state, price the retry. Replication is often more expensive than the thing it prevents, and always more expensive to reason about.

Recovery

The 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

Status: Accepted · Shown on views: 05, 15, 12

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.

Option Verdict Reasoning
Classified routes, mandatory credential enrolment Chosen The account's real assurance becomes visible and actionable; costs a configuration obligation on tenants and a contract obligation on relying parties
Recovery as an unclassified side path Rejected Issues a session indistinguishable from a strong one, so the weakest route silently sets every account's security level
Mandate a second passkey at enrolment and support nothing else Rejected The strongest design and commercially unacceptable: the enrolment friction loses the users the migration exists to convert
Leave recovery entirely to the tenant Rejected Makes the platform's phishing-resistance claim unfalsifiable, since the real authentication path would be outside it

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.

Lesson. Strengthening 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.

ADR-11 · A credential enrolled through recovery waits before it may revoke anything

Status: Accepted · Shown on views: 05, 15, 19

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.

Option Verdict Reasoning
Assurance-scaled cooling-off on revocation rights only Chosen Removes the immediate-eviction attack while keeping the account usable; costs the legitimate owner a wait at the worst possible moment
No restriction Rejected Lets a successful takeover evict the owner using the owner's own security feature, which makes recovery a net security loss
Require two existing credentials to revoke Rejected Impossible for the single-credential user, who is precisely the population that needs recovery
Human review of every post-recovery revocation Rejected Correct and unscalable; it also puts the support desk back in the position of being the real authenticator

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.

Lesson. When you cannot tell two actors apart, stop trying and make the attacker's move require patience the honest user can afford to give.

Boundaries and tenancy

Where 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

Status: Accepted · Shown on views: 01, 09, 21

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.

Option Verdict Reasoning
Signed verdict, relying party issues the session Chosen Bounds the consequence of a platform compromise and removes the largest adoption barrier; costs the platform any ability to revoke a session
Full identity provider issuing sessions and tokens Rejected Makes a platform compromise a compromise of every tenant's live sessions, and requires replacing session infrastructure tenants already trust
Both, tenant's choice Rejected Two security models, two blast radii and two sets of revocation semantics to reason about; the flexibility is bought with permanent ambiguity
Opaque receipt the relying party must call back to redeem Rejected Puts this platform on the critical path of every session establishment, so its outage becomes the tenant's outage

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.

Lesson. Decide whether you are producing evidence or producing authority. Evidence is cheaper to be wrong about, and far cheaper to be breached with.

ADR-13 · Revocation is a state change, and a policy change is never retroactive

Status: Accepted · Shown on views: 12, 17, 19

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.

Option Verdict Reasoning
Revocation as state change; policy forward-only with an impact gate Chosen Routine tightening is safe and destructive action is deliberate; costs a two-approver flow and an analysis gate to build and maintain
Policy changes apply retroactively Rejected Makes every routine configuration edit a potential mass-lockout event with no review step in front of it
Revocation by deleting the credential row Rejected Destroys the evidence of why a credential stopped working, which is precisely what a management-plane compromise would want to do
Soft-delete with a retention job that eventually purges Rejected Reintroduces deletion on a timer, so the audit trail's lifetime is set by a cleanup policy rather than by the retention requirement

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.

Lesson. A 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.

Status: Accepted · Shown on views: 01, 09, 12

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.

Option Verdict Reasoning
One tenant-owned RP ID, related origins for brands Chosen One credential across the estate on a domain the tenant owns; costs a declared trust relationship between brand domains and a divestment problem
RP ID per brand Rejected Clean separation, and users must enrol separately on each brand, which a shared-account estate experiences as the feature not working
Platform's own domain as a shared RP ID Rejected Operationally simplest and makes this platform the trust anchor for every tenant's authentication, which is a lock-in a tenant should refuse
Let each tenant choose freely with no default Rejected 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

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.

Lesson. Ask 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.

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

Status: Accepted · Shown on views: 06, 17, 18

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.

Option Verdict Reasoning
Tenant-set threshold, platform-enforced gate, attributed override Chosen 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
No gate: cutover is purely the tenant's call Rejected Lets a security decision be taken without the support consequence being visible, which is how passwordless migrations acquire their reputation
A platform-set threshold Rejected Substitutes the platform's judgement for the tenant's about their own users' tolerance and their own support capacity
Advisory warning only Rejected A warning in a console is read by the person clicking the button, which is rarely the person who will answer the calls

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.

Lesson. Do not block the decision; block the decision being made without the number. And make the override cost a name, not a click.

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.

Package What it is What it does here Considered 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.