Skip to content

rpID setup — WebAuthn HTTPS strategy

Stellar Passkey Kit — developer reference. Sources: W3C WebAuthn L3 §5.1.3 (rpId scoping), Cloudflare Tunnel docs (named & quick tunnels).

WebAuthn passkey ceremonies (navigator.credentials.create() and .get()) require a secure context — that means HTTPS, or the literal hostname localhost / 127.0.0.1. Anything else (a LAN IP, 192.168.x.x, a co-worker's machine over HTTP, a random preview URL on plain HTTP) is rejected by the browser before a prompt ever appears.

This document explains the three environments where we run passkey flows — production, preview / CI, and local dev — and the rpId rules that bind them together.


1. rpId scoping rules — read this first

The Relying Party ID (rpId) is the registrable suffix that a passkey is bound to. The browser enforces these rules and silently no-ops if they are violated:

  1. rpId MUST be a valid domain string (no scheme, no port, no path).
  2. rpId MUST equal window.location.hostname or be a registrable parent of it. Examples below.
  3. A passkey created with rpId = X will only be usable on origins whose hostname is X or a subdomain of X. There is no cross-registrable-domain portability — by design.
  4. The "registrable" part means the eTLD+1 boundary (the Public Suffix List). You cannot set rpId = "dev" or rpId = "pages.dev" — those are public suffixes, not registrable.
  5. Default when rpId is omitted: the browser uses window.location.hostname verbatim.
Page is served fromAllowed rpId valuesDisallowed
passkey.example.devpasskey.example.dev, example.devdev, example.com, other.example.dev
demo.example.devdemo.example.dev, example.dev(cannot use passkey.example.dev)
app.pages.devapp.pages.devpages.dev (public suffix)
localhostlocalhostanything else

Practical rule for this project: pick the broadest rpId that covers every origin where the kit will be used. For the SCF-43 sprint we use passkey.<yk-domain> per environment and set rpId to the eTLD+1 so passkeys made in preview also work in production once the user is ready to migrate.


2. Production

  • Hosting: Cloudflare Pages.
  • Domain: passkey.<yk-domain> (registered Cloudflare zone, e.g. passkey.yk-labs.dev — final domain TBD by YK-239 follow-up).
  • rpId: the eTLD+1, e.g. yk-labs.dev. Allows preview and production subdomains to share a credential namespace if we ever migrate one.
  • HTTPS: automatic via Cloudflare's edge cert. Nothing to configure in app code.

Demo deploys are triggered from main of apps/demo and land at https://passkey.<yk-domain> via the Pages project bound to that subdomain.

3. Preview / CI

  • Hosting: Cloudflare Pages preview deployments (per-PR URL).
  • Domain pattern: https://<sha>.<project>.pages.dev.
  • rpId: <project>.pages.devbut pages.dev itself is a public suffix, so we must set rpId = <project>.pages.dev exactly (not just pages.dev).
  • Caveat: passkeys created against <project>.pages.dev are usable on any subdomain of that project only. They will not transfer to the production rpId, which is fine — preview credentials are throwaway by design.

CI uses the same preview URL when running browser-based smoke tests; the Playwright virtual-authenticator does not need DNS, but Safari-via-safaridriver does, so the preview URL is the canonical CI target.

4. Local dev — three options

4.1 Plain localhost (fastest)

Run the Vite demo:

bash
pnpm --filter @stellar-passkey/demo dev
# → http://localhost:5173

localhost is a secure context. Set rpId = "localhost". Credentials made here only work on localhost — they do not transfer to staging or production.

4.2 Quick tunnel (no Cloudflare login)

For testing on a phone, in a VM, or with Safari, use a quick tunnel. Hostname is random and ephemeral; great for ad-hoc checks:

bash
cloudflared tunnel --url http://localhost:5173
# Logs print a URL like:
#   https://crispy-fox-1234.trycloudflare.com

Set rpId to that random hostname. Credentials are throwaway — the URL dies when you kill cloudflared.

4.3 Named tunnel with stable hostname (best for shared dev)

Once a Cloudflare account is wired up and a domain exists on Cloudflare:

bash
cloudflared tunnel login                  # opens browser, picks the zone
cloudflared tunnel create stellar-passkey
cloudflared tunnel route dns stellar-passkey passkey.<your-domain>
cloudflared tunnel --config cloudflared/config.yml run stellar-passkey

See cloudflared/config.example.yml for the file the last command expects. The tunnel exposes http://localhost:5173 at https://passkey.<your-domain> with a real cert. Set rpId = "<your-domain>" so credentials made locally also work in preview/production.

The first three commands are one-time setup per workstation. After that, cloudflared tunnel run stellar-passkey is the only command you'll re-run.

5. SDK convention

Code that initiates a ceremony must read rpId from configuration, not hard-code the production domain. Example:

ts
import { startCreate } from "@stellar-passkey/core";

await startCreate({
  rpId: import.meta.env.VITE_RP_ID ?? window.location.hostname,
  rpName: "Stellar Passkey Kit Demo",
  // …
});

VITE_RP_ID is read from .env.local (gitignored) in dev, and from Cloudflare Pages env vars in preview/production.

6. Gotchas captured from past incidents

  • Setting rpId to a public suffix (e.g. "pages.dev", "dev") — the browser rejects the ceremony with a generic NotAllowedError. Always pick the eTLD+1 or a deeper label.
  • Mixing rpIds across environments — a passkey made with rpId = "localhost" is unusable on passkey.example.dev and vice versa. Plan the rpId before you ship the first credential into a user's keychain.
  • Mobile testing via LAN IP — does not work. Use a tunnel (§4.2 or §4.3) to get HTTPS to the phone.
  • CI flake from random tunnel hostnames — the rpId per CI job changes every run. Use a named tunnel for any test asserting on rpId persistence.
  • Apple Passkeys and rpId change — if you change the rpId of an existing relying party, Apple's keychain treats the new and old as separate apps; user has to enroll a new passkey.

7. Status (YK-239)

  • [x] docs/dev/rpid-setup.md written (this file).
  • [x] cloudflared ≥ 2026.2.0 installed locally.
  • [x] cloudflared/config.example.yml template committed.
  • [ ] Cloudflare zone selected and passkey.<domain> DNS routed — pending: domain choice + cloudflared tunnel login (requires browser auth as the Cloudflare account owner).
  • [ ] curl -fsSL https://passkey.<domain> returns 200 — pending the step above.

MIT — SCF-43 RFP submission (2026). Status: pre-1.0.