Claiming an ID
How a person with nothing — no identity, no CLI, no invitation to a particular relay — ends up signed in. The short version: you pick a name, the relay says yes before you commit anything, and one passkey carries you from the signup host to your own.
For the person signing up
- Open the launcher. A relay that hosts identities serves the app at
id.<its domain>(id.poweur.netfor the reference deployment) — the same app you will use afterwards, in no-identity mode. - Type a name. It is checked as you type. "Taken", "reserved by the operator", "use only a-z, 0-9 and hyphen" — whatever the answer is, it arrives before anything else happens, and the Next button stays disabled until the relay says the name is free.
- Create a passkey with PRF. Touch ID, Face ID, Windows Hello, or a security key that implements the WebAuthn PRF extension (Apple Passwords / iCloud Keychain, or Google Password Manager). Password-manager passkeys that cannot emit PRF are refused — there is no PIN fallback. This is the only ceremony in the flow.
- You land on your own address.
alice.poweur.net/app/, signed in, with the setup flow (walkthrough) waiting.
Step 4 needs no second passkey: the credential is scoped to the domain the launcher and your identity share, so the one you just made opens on both. See credential scope.
If the relay gates signup behind proof-of-work, step 3 says so and shows the work happening rather than appearing to hang.
Why the order matters
Checking the name after the passkey is the obvious way to build this and the wrong one:
a WebAuthn ceremony is a real interruption, and a credential created for a name you cannot
have is litter in the user's authenticator that nobody ever cleans up. So the relay
answers first, through
GET /hosted/availability, and the answer
carries the operator's policy so the app can validate the next attempt inline without
another round trip.
For operators
The launcher is not a second application. It is the same static tree, served on a dedicated host so that someone with no identity has somewhere to land:
LAUNCHER_HOST(defaultid.<first hosted domain>) — the relay redirects/there to/app/, and advertises the host atGET /so a client knows where it is running.idandlauncherare reserved labels, so the launcher host can never collide with an identity somebody claimed.- Name rules — length, reserved names, a blocked-terms file — are
configuration. The character set is not:
handles are ASCII
a-z 0-9 -on every deployment, because a handle becomes a DNS label and a name under your wildcard certificate. - A self-hoster needs none of this. Their relay serves the app at
<identity>/app/from the released image; there is no fork to maintain and nothing to publish to an app store.
What the hand-off carries
The identity record moves from the launcher host to the identity's origin in the URL
fragment, which browsers never send to a server. What travels is the same AES-GCM blob
the launcher had in localStorage — the wrapping secret comes from the passkey and is
not in it — and the receiving page clears the fragment as soon as it has stored it, so
the blob does not linger in the address bar or in history.