ADR 0012 — Console WebAuthn: Separate RP ID (console.raxx.app)
Status: Accepted (2026-04-22)
Date: 2026-04-22
Deciders: user + software-architect
Scope: WebAuthn RP ID for the operator admin console (console.raxx.app), separate from the user-facing web + iOS apps (which share raxx.app per ADR 0005).
Context
Raxx's user-facing surfaces (web at raxx.app, iOS companion app) share the WebAuthn RP ID raxx.app per ADR 0005 — passkeys registered by a user on the web auto-work on iOS via iCloud Keychain, no re-registration.
The operator admin console (console.raxx.app) is a separate application serving a distinct population: staff / operators with elevated permissions (RBAC roles superadmin, ops, support, readonly). Admin identities are NOT the same as end-user identities, even when a single person (e.g., the founder) holds both.
The decision: should the console share raxx.app as its RP ID (inheriting user-app passkeys with server-side admin-lookup rejecting non-admin credentials at login), or use a separate RP ID?
An earlier framing on 2026-04-22 leaned toward shared RP with RBAC as the gate. This ADR records the final, reversed decision: separate.
Decision
Use console.raxx.app as the RP ID for the admin console. Passkeys registered at the console live under a different origin-scoped credential pool than passkeys registered at raxx.app. Cross-contamination between user and admin credentials is prevented at the browser layer, not just at the server-side admin-lookup layer.
User-facing Raptor: WEBAUTHN_RP_ID=raxx.app, WEBAUTHN_ORIGIN=https://raxx.app (unchanged — per ADR 0005)
If the console ever adds an iOS companion app: it gets its own AASA file at https://console.raxx.app/.well-known/apple-app-site-association. No iOS companion is in scope for console v1.
Consequences
Positive
Defense in depth. Browser-level origin-matching refuses to offer user credentials at the console origin and vice versa. Server-side admin-lookup becomes belt-and-suspenders rather than the last line of defense.
Hardware-key role binding. Admins can carry a dedicated physical key labeled for console use, registered only at console.raxx.app. Even with multiple credentials on one YubiKey, browser origin matching prevents cross-prompt leakage — physical separation reinforces role identity (tap-this-key-only-here).
Future-proof for multi-admin onboarding. When a non-founder admin is added, they register console passkeys without any intersection with user credentials. No dependency on Kris's personal user passkey.
Pre-launch cost of separation: near zero. No passkeys registered yet (card #110 WebAuthn registration endpoint is ready-for-dev, not shipped). Changing the RP ID post-registration would force re-registration; changing now is just a config string.
Aligns with principle of least surprise. Admin credentials and user credentials are different populations; different populations → different credential pools.
Negative / costs
Kris (and any future person who is both user and admin) must register two separate passkeys — one at raxx.app, one at console.raxx.app. One-time ceremony per device; modern authenticators (YubiKey 5, iCloud Keychain) support unlimited credentials.
Two AASA files if and when a console iOS companion app is added (not v1). Small operational cost.
Apple's WWDC 2022 "cross-platform passkey" recommendation implicitly assumed a single user population. Our split diverges slightly — but for a distinct admin population, that divergence is a feature.
Neutral
Console's Raptor admin API JWT verification is unaffected. JWT scope is orthogonal to RP ID; both still work.
iCloud Keychain sync still works per-RP-ID — passkeys registered at raxx.app sync across the user's Apple devices; passkeys registered at console.raxx.app sync separately. Both work.
Alternatives considered
Share raxx.app RP ID across web + iOS + console, rely on server-side admin-lookup
Briefly leaned toward on 2026-04-22, then reversed. Reasons rejected:
Browser still offers user credentials at console login prompt (all raxx.app-scoped credentials are eligible; server rejects non-admin ones post-presentation). Noisy UX.
Relies on a single gate (server-side admin-lookup) to enforce a security boundary the browser's origin-scoping could enforce natively.
Couples admin and user credential lifecycles (revoking one scope doesn't automatically scope the other).
Inconsistent with the separate-app, separate-Postgres, separate-codebase blast-radius isolation used elsewhere in console design (see docs/architecture/console.md).
Use a non-subdomain RP ID (e.g., raxx-console.com)
Rejected. Keeping console under the same apex (raxx.app subdomain) preserves the brand + DNS + TLS ops story. A separate-domain RP ID adds operational cost without security benefit over a subdomain RP ID.
Same RP ID but different "user verification" requirements for admins
Rejected. WebAuthn user-verification modifiers don't partition credentials by role; they're per-assertion flags. Not a substitute for origin isolation.
Compliance checklist
[x] No new credential storage pattern — each RP scope has its own webauthn_credentials table (or equivalent), which is the already-designed pattern from ADR 0001.
[x] AASA file (if/when console iOS app added) does not expose PII.
[x] RP ID change after registration is blocked by design — neither user nor admin can migrate credentials across RP IDs; this matches the web-side constraint from ADR 0005.
Revisit when
A user-directed single-sign-on requirement emerges where admin login from user credentials is genuinely desired (unlikely by design, but possible if ops tooling consolidates).
The console adds an iOS companion app (would require a console-scoped AASA file at console.raxx.app).
An incident reveals credential-pool overlap / contamination; this ADR confirms the existing separation is the defense mechanism.
Implementation notes
Zero-migration deployment. No users or admins have registered passkeys yet as of this ADR's date. Decision is purely a pre-launch config choice.
Card #110 (WebAuthn registration endpoint for Raptor) should specify WEBAUTHN_RP_ID=raxx.app explicitly in its acceptance criteria.
Console implementation sub-card (not yet filed — pending PM breakdown of console epic #146) should specify WEBAUTHN_RP_ID=console.raxx.app explicitly.
Env vars are app-scoped, not shared. Each deployed app (Raptor, future console) has its own WEBAUTHN_RP_ID in its own Heroku config. No cross-app coordination needed.