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.
Configuration:
- Console app config:
WEBAUTHN_RP_ID=console.raxx.app,WEBAUTHN_ORIGIN=https://console.raxx.app - 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 atconsole.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.appsync across the user's Apple devices; passkeys registered atconsole.raxx.appsync 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_credentialstable (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.appexplicitly 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.appexplicitly. - Env vars are app-scoped, not shared. Each deployed app (Raptor, future console) has its own
WEBAUTHN_RP_IDin its own Heroku config. No cross-app coordination needed.