aswad

How to Add Passkeys to Your Website (Without a PhD)

Passkeys come down to three named parts, two ceremonies, and one build-vs-buy decision. A founder's plain guide to adding them to your site without reading the WebAuthn spec at 2am.

Developer's keyboard and code on screen representing adding passkeys to a website

A founder I know shipped his own WebAuthn implementation last spring. He's a sharp engineer, the kind who reads RFCs for fun, and he figured passkeys couldn't be that hard. Six weeks later he had a working login, a Slack channel full of edge-case bug reports, and the haunted expression of a man who now knows more about CBOR encoding than any human should. He pulled it out and bought the boring thing instead. I tell that story not to scare you off, but because it's the most honest framing I have: adding passkeys to your site is genuinely doable, and you almost certainly shouldn't build the hard part yourself.

Let me untangle what's actually going on, because the jargon makes this sound like a graduate seminar when it's really just three moving parts in a trench coat. Once you can name the pieces, the whole thing stops being intimidating, and you can make a clear-eyed decision about how much of it you want to own. Spoiler: less than you think.

Abstract circuit board with glowing login pathways
Three moving parts in a trench coat. Let's open the coat.

The three moving parts

The first piece is the relying party, which is a deliberately pompous name for "your website." That's it. When the spec says relying party, it means you — the site that's relying on a passkey to vouch for a user. The relying party has an ID, which is basically your domain, and that domain-binding is the secret sauce of the whole system. A passkey created for yoursite.com physically refuses to work on yoursiit.com, which is why phishing dies. The browser does that check for you. You don't write a line of anti-phishing logic; geography is enforced by math you didn't have to author.

The second piece is the pair of WebAuthn ceremonies. There are exactly two, and "ceremony" is just the spec's lofty word for "handshake." Registration is when a user creates a passkey on your site: their device mints a fresh key pair, keeps the private half locked in hardware, and hands you the public half to store. Authentication is when they come back: you send a random challenge, their device signs it with that private key, and you check the signature against the public key you saved. That's the entire dance. Create a key, then later prove you still have it. Everything else is plumbing around those two moments.

The third piece is a backend that stores public keys. This is the part people overcomplicate. You are not storing secrets. The public key is, by design, safe to leave lying around — it's useless without the private key that never left the user's phone. So your database table is almost insultingly mundane: a user ID, their public key, a credential ID, and a signature counter you bump on each login. If that table leaks tomorrow, the attacker gets a pile of public keys they can do nothing with. Compare that to leaking a table of password hashes and watching Twitter set itself on fire.

So why did my friend suffer?

Because the gap between "two ceremonies and a boring table" and "a correct, browser-compatible, attack-resistant implementation" is enormous, and it's all in the details nobody warns you about. The browser doesn't hand you a clean signature and a username. It hands you a nested binary blob encoded in CBOR, wrapped in base64url, containing an attestation statement you have to parse, validate, and decide whether to trust. You have to verify the challenge you sent is the challenge that came back, that the origin matches, that the relying party hash is right, that the signature counter went up and not down (a clone detector), and that the user-verification flags say what you require them to say.

The crypto in WebAuthn is solved and beautiful. The plumbing around it is where careers go to get tired.

Build versus buy, decided honestly

Here's my actual advice, founder to founder, with no horse in the race beyond wanting you to ship something that works. If passkeys are core to your product — if you're building a security company, a wallet, an identity layer, something where owning every byte of the auth flow is the point — then build it, and budget real engineering time, not a sprint. You'll learn a lot and you'll need to.

For everyone else, which is most of us, use a library for the verification logic at minimum, and seriously consider an identity provider for the whole thing. A good server-side WebAuthn library (SimpleWebAuthn in the Node world, py_webauthn in Python, and friends) handles the CBOR-parsing, signature-checking, counter-tracking nightmare so you write maybe thirty lines of glue instead of three thousand lines of bugs. That alone takes "six haunted weeks" down to "a focused afternoon." It's the highest-leverage dependency you'll add all year.

An identity provider goes further and runs the ceremonies, stores the keys, handles the device quirks, and gives you back a logged-in user. This is the "Sign in with Paswad" model, and it exists precisely so you never have to read the WebAuthn spec at 2am. You drop in a button, point it at your domain, and the hard, breakable, attack-surface-heavy parts live with people who do nothing but that. Paswad is my version of that bet, but the broader point stands regardless of whose logo is on the button: implementing WebAuthn from scratch is a fine hobby and a poor default.

What the flow actually looks like

Here's the entire authentication ceremony in pseudo-code. Notice how little of it is yours to write when a library handles the middle.

// 1. User clicks "Sign in"
challenge = server.makeRandomChallenge()   // fresh, single-use

// 2. Browser asks the device to sign it
assertion = navigator.credentials.get({
  publicKey: { challenge, rpId: "yoursite.com" }
})
// device unlocks private key with Face/Touch ID, signs locally

// 3. Server checks the proof
ok = verify(assertion.signature,
            storedPublicKey,
            challenge,            // must match what we sent
            origin = "yoursite.com")
if (ok) logUserIn()              // no secret ever crossed the wire

That's the whole shape of it. A random challenge goes out, a signature comes back, you verify it against a key you already stored. The biometric unlock happens entirely on the user's device — you never see a fingerprint, and you never receive anything you'd be afraid to have leaked.

Roll it out without breaking everything

Whatever you do, do not flip a switch that makes passkeys the only way in on launch day. That's how you generate a support queue the length of a CVS receipt. The sane rollout is additive: keep your existing login exactly as it is, and offer passkeys as a second, better option alongside it. After someone signs in the old way, show a gentle "set up a passkey for faster sign-in?" prompt. Let the early adopters opt in, watch your metrics, fix the rough edges with a small audience instead of your whole user base.

Smartphone displaying a secure passkey prompt
Offer it as the better option, not the only one. At first.

Over weeks and months, you'll see the passkey adoption curve climb on its own, because the experience is genuinely nicer and people are not stupid. Once a healthy majority of active users have a passkey, you can start nudging the stragglers, and eventually make passwords the awkward fallback instead of the front door. The order matters: earn the trust with a smooth optional experience first, then tighten the screws. Reverse that and you'll spend your goodwill on a migration nobody asked for. Passkeys sell themselves once people have one. Your only real job is to not make the first one annoying to create.

Frequently asked questions

Do I have to drop passwords to offer passkeys?

No, and you shouldn't at first. Passkeys live happily alongside passwords as an additional, stronger option. Offer them as an upgrade, let adoption build, and only later consider making passwords a fallback or retiring them. A hard cutover on day one is the single most common way these rollouts go badly.

What do I actually need to store on my server?

Very little, and none of it is secret. Per user you keep their public key, a credential ID, and a signature counter. Public keys are safe to store in the clear because they're useless without the private key, which never leaves the user's device. There's no password hash to protect and nothing breach-worthy in that table.

Should I implement WebAuthn myself or use a provider?

Unless auth is your core product, use a server-side WebAuthn library at minimum, or an identity provider for the whole flow. The cryptography is the easy part; the CBOR parsing, attestation checks, signature-counter logic, and cross-browser quirks are where the time and the bugs hide. A provider or library turns weeks of fragile work into an afternoon of glue code.

Will passkeys work across all my users' devices?

The major platforms — iOS, Android, macOS, Windows, and the big browsers — all support WebAuthn now, and passkeys sync through platform keychains so users keep them across devices. Hardware keys cover the edge cases. The remaining per-platform UI differences are exactly what a library or provider smooths over for you.

Adding passkeys isn't a PhD project. It's three named parts, two ceremonies, and one decision about how much plumbing you want to personally own. Make that decision with your eyes open, roll it out alongside what you've already got, and you'll be fine. My friend got there too. He just took the scenic, haunted route, and now he tells the story so you don't have to live it.