SimpleWebAuthn Tutorial: Passkeys in Node.js (v13 Guide)

A complete SimpleWebAuthn tutorial: add passkey login to a Node.js app with v13 — registration ceremony, authentication ceremony, the rpId/expectedOrigin gotchas, conditional UI autofill, and exactly what to store per credential.

Passwords didn't lose because they were weak. They lost because phishing steals any secret you can type, and because reuse turns one breach into fifty. WebAuthn fixes the actual disease — credentials are cryptographically bound to your domain, so a phishing site gets nothing usable — but for years the API was a week of spec-reading. SimpleWebAuthn v13 changed that: the whole thing is now two ceremonies, two packages, and a weekend project. This SimpleWebAuthn tutorial walks you through both ceremonies in Node.js, the two gotchas that break every first deploy, passkey autofill, and exactly what to store in your database. What you'll build and learn Set up the @simplewebauthn/server and @simplewebauthn/browser v13 packages and learn the v13 call shape (it broke v10 code) Implement the registration ceremony: generate options → browser creates the passkey → server verifies Implement the authentication ceremony: generate options → browser signs the challenge → server verifies the signature Dodge the two gotchas that break every first deploy: rpId and expectedOrigin Add passkey autofill (conditional UI) so users sign in from the browser's autocomplete dropdown Design the credential storage schema: what to save per passkey and why the counter matters Setup: The Two Packages and the v13 Call Shape SimpleWebAuthn splits cleanly: @simplewebauthn/server generates and verifies everything on Node, @simplewebauthn/browser talks to the authenticator on the client. Install both at 13.x (Node 22 LTS works fine): npm install @simplewebauthn/server @simplewebauthn/browser One critical v13 note before any code: since v11, the browser functions take an options object, not bare options. If you copy v10-era code like startRegistration(options) , v13 throws a "not called…

Explore