[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-webauthn-passkeys-discoverable-credentials":44,"search-suggestions":60,"quiz-article-webauthn-passkeys-discoverable-credentials":107},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a07112-8d47-75a9-a557-077e1a136955","webauthn-passkeys-discoverable-credentials","PRACTICE_QUIZ","WebAuthn & Passkeys","Check what actually turns a WebAuthn credential into a passkey — and why the underlying signature is phishing-resistant in a way a password never can be.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,96,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",88,{"slug":66,"name":67,"articles":68},"javascript","Javascript",72,{"slug":70,"name":71,"articles":72},"frontend","Frontend",69,{"slug":74,"name":75,"articles":76},"css","Css",31,{"slug":78,"name":79,"articles":80},"tutorial","Tutorial",19,{"slug":82,"name":83,"articles":84},"performance","Performance",12,{"slug":86,"name":87,"articles":88},"typescript","Typescript",11,{"slug":90,"name":91,"articles":92},"react","React",10,{"slug":94,"name":95,"articles":51},"browser","Browser",{"slug":97,"name":98,"articles":99},"grammar","Grammar",6,{"slug":101,"name":102,"articles":103},"html","Html",5,{"slug":105,"name":106,"articles":103},"node","Node",{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":99,"publishedAt":112,"viewCount":113,"likeCount":19,"commentCount":19,"author":114,"vertical":119,"topic":120,"tags":123,"_count":130,"playground":132,"body":134,"bodyMd":275,"seo":276,"translationGroupId":279,"series":52,"podcastUrl":52,"verticalId":5,"thread":280,"assessments":282,"translations":285,"quiz":287},"01a07112-8d09-7178-b258-b899a0eb496e","I Shipped 'Passwordless Login.' It Still Asked for a Username.","WebAuthn's own demo code creates a credential the browser can't find on its own — so your passkey button quietly turns into a fancier security key instead of an actual passwordless sign-in. One option fixes it.","\u002Fmedia\u002Fcovers\u002Fwebauthn-passkeys-discoverable-credentials.png","2026-09-10T10:57:59.771Z",21,{"id":115,"name":116,"username":117,"avatarUrl":52,"headline":118},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":121,"name":122},"security","Security",[124,125,126,127],{"slug":62,"name":63,"color":52},{"slug":121,"name":122,"color":52},{"slug":66,"name":67,"color":52},{"slug":128,"name":129,"color":52},"webauthn","Webauthn",{"assessments":131},1,{"slug":46,"title":133},"WebAuthn: passkey vs. plain credential — interactive playground",{"blocks":135,"version":131},[136,140,143,148,151,157,160,163,166,169,175,178,182,185,188,191,194,197,200,203,206,210,213,216,219,225,228,231,236,239,242,245,248,251,254,257,260,263,266,269],{"id":137,"html":138,"type":139},"b1","\u003Cp>I shipped &quot;Sign in with a passkey&quot; on a side project last month. Fingerprint reader popped up, the credential got created, login worked. I was pretty proud of it — right up until my partner tried it and typed her email address first, the way she does for every other login, and only \u003Cem>then\u003C\u002Fem> got the fingerprint prompt.\u003C\u002Fp>","paragraph",{"id":141,"html":142,"type":139},"b2","\u003Cp>That&#39;s not a passkey. That&#39;s a security key with extra steps. The whole pitch of a passkey is that the browser already knows who&#39;s trying to sign in — no username field, no typing, just a tap. Mine still needed the name first. And the code I&#39;d written looked exactly like the WebAuthn examples everyone links to.\u003C\u002Fp>",{"id":144,"html":145,"text":146,"type":147,"level":31},"b3","The code that &quot;works&quot;","The code that \"works\"","heading",{"id":149,"html":150,"type":139},"b4","\u003Cp>Here&#39;s roughly what I had, trimmed to the part that matters:\u003C\u002Fp>",{"id":152,"code":153,"type":154,"language":155,"highlight":156},"b5","const credential = await navigator.credentials.create({\n  publicKey: {\n    challenge: randomChallengeFromServer,\n    rp: { name: \"My App\", id: \"myapp.com\" },\n    user: {\n      id: userIdBytes,\n      name: \"you@example.com\",\n      displayName: \"You\",\n    },\n    pubKeyCredParams: [{ alg: -7, type: \"public-key\" }],\n  },\n});","code","js",[],{"id":158,"html":159,"type":139},"b6","\u003Cp>This is correct WebAuthn. It creates a real public\u002Fprivate key pair, stores the private half on the authenticator, and hands you back a public key to save on your server. Signing in with it later works fine — \u003Cem>if\u003C\u002Fem> you already know which user is signing in and can send back the specific \u003Ccode>credential.id\u003C\u002Fcode> you stored for them.\u003C\u002Fp>",{"id":161,"html":162,"type":139},"b7","\u003Cp>That &quot;if&quot; is the whole bug. Nothing here tells the authenticator to save this credential somewhere the browser can find \u003Cem>on its own\u003C\u002Fem>. So at login time, you&#39;re stuck asking for a username first, looking up that user&#39;s stored credential ID, and only then calling \u003Ccode>navigator.credentials.get()\u003C\u002Fcode> with it. Which is exactly the flow I&#39;d built — technically WebAuthn, functionally a slower 2FA prompt.\u003C\u002Fp>",{"id":164,"html":165,"text":165,"type":147,"level":31},"b8","Discoverable vs. non-discoverable — the distinction the tutorials skip",{"id":167,"html":168,"type":139},"b9","\u003Cp>A WebAuthn credential can be one of two things, and the spec&#39;s own names for them undersell how different they are in practice:\u003C\u002Fp>",{"id":170,"type":171,"items":172,"ordered":18},"b10","list",[173,174],"\u003Cstrong>Non-discoverable (a &quot;server-side&quot; credential).\u003C\u002Fstrong> The credential is registered, but nothing about it is stored \u003Cem>on the authenticator\u003C\u002Fem> in a way the browser can enumerate. To sign in, your server has to already know the user (from a username, a cookie, whatever) and hand back that user&#39;s specific credential ID in the \u003Ccode>get()\u003C\u002Fcode> call.","\u003Cstrong>Discoverable (what people mean by &quot;passkey&quot;).\u003C\u002Fstrong> The credential itself is saved on the device — in the platform&#39;s keychain or the security key&#39;s own storage — with enough metadata that the browser can show it in a picker with \u003Cstrong>no prior input at all.\u003C\u002Fstrong> Type nothing, get a list of &quot;sign in as ___,&quot; tap one, done.",{"id":176,"html":177,"type":139},"b11","\u003Cp>Spot the fix yet? It&#39;s one field, in the options object you already have:\u003C\u002Fp>",{"id":179,"code":180,"type":154,"language":155,"highlight":181},"b12","const credential = await navigator.credentials.create({\n  publicKey: {\n    challenge: randomChallengeFromServer,\n    rp: { name: \"My App\", id: \"myapp.com\" },\n    user: { id: userIdBytes, name: \"you@example.com\", displayName: \"You\" },\n    pubKeyCredParams: [{ alg: -7, type: \"public-key\" }],\n    authenticatorSelection: {\n      residentKey: \"required\",       \u002F\u002F ← this is the whole fix\n      userVerification: \"preferred\", \u002F\u002F ask for biometric\u002FPIN, don't require it\n    },\n  },\n});",[],{"id":183,"html":184,"type":139},"b13","\u003Cp>\u003Ccode>residentKey: &quot;required&quot;\u003C\u002Fcode> tells the authenticator: don&#39;t just hand back a key, \u003Cem>keep a copy of yourself here\u003C\u002Fem> so the browser can find you later without being told your name. That&#39;s the difference between &quot;a WebAuthn credential&quot; and &quot;a passkey&quot; — the second is really just the first, stored discoverably.\u003C\u002Fp>",{"id":186,"html":187,"type":139},"b14","\u003C!-- playground:start -->",{"id":189,"html":190,"text":190,"type":147,"level":31},"b15","🎮 Try it yourself",{"id":192,"html":193,"type":139},"b16","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fwebauthn-passkeys-discoverable-credentials\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":195,"html":196,"type":139},"b17","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":198,"html":199,"type":139},"b18","\u003C!-- playground:end -->",{"id":201,"html":202,"text":202,"type":147,"level":31},"b19","The other half: how sign-in actually skips the username field",{"id":204,"html":205,"type":139},"b20","\u003Cp>Making the credential discoverable only gets you halfway. The login call still has to \u003Cem>ask\u003C\u002Fem> for a discoverable credential instead of a specific known one — and there&#39;s a second option most people never see because it doesn&#39;t live in the WebAuthn spec at all. It&#39;s part of the separate Credential Management API, layered on top:\u003C\u002Fp>",{"id":207,"code":208,"type":154,"language":155,"highlight":209},"b21","const credential = await navigator.credentials.get({\n  publicKey: {\n    challenge: randomChallengeFromServer,\n    \u002F\u002F no allowCredentials list — let the browser show whatever it has\n  },\n  mediation: \"conditional\",\n});",[],{"id":211,"html":212,"type":139},"b22","\u003Cp>\u003Ccode>mediation: &quot;conditional&quot;\u003C\u002Fcode> is what makes a passkey show up \u003Cstrong>inside the browser&#39;s own autofill dropdown\u003C\u002Fstrong> on a plain \u003Ccode>&lt;input autocomplete=&quot;username webauthn&quot;&gt;\u003C\u002Fcode> field, sitting right next to any saved passwords — instead of requiring a separate &quot;Sign in with passkey&quot; button the user has to notice and click. Drop the button. Let the username field do it. That&#39;s the UX every passkey demo video is showing you, and it&#39;s a call that has nothing to do with \u003Ccode>residentKey\u003C\u002Fcode> — you need both.\u003C\u002Fp>",{"id":214,"html":215,"text":215,"type":147,"level":31},"b23","Why this is more than a UX nitpick",{"id":217,"html":218,"type":139},"b24","\u003Cp>The reason WebAuthn is worth this much ceremony isn&#39;t the fingerprint reader — Touch ID logins existed before this API. It&#39;s what happens underneath, on every sign-in:\u003C\u002Fp>",{"id":220,"type":171,"items":221,"ordered":17},"b25",[222,223,224],"Your server sends a fresh, random \u003Cstrong>challenge\u003C\u002Fstrong>.","The authenticator signs that challenge with the private key — the one that has never left the device and never will.","Critically, the signature also covers the \u003Cstrong>origin\u003C\u002Fstrong> the browser is actually on. A credential created for \u003Ccode>myapp.com\u003C\u002Fcode> produces a signature that a server for \u003Ccode>myapp.com\u003C\u002Fcode> accepts — and a phishing page at \u003Ccode>myapp-login.com\u003C\u002Fcode> cannot get a valid one out of the same authenticator, because the browser includes the real origin, not whatever the page claims to be.",{"id":226,"html":227,"type":139},"b26","\u003Cp>Password managers that autofill by domain get phished anyway when a user manually copy-pastes. WebAuthn doesn&#39;t give the user that option — there&#39;s no password to copy. The credential simply doesn&#39;t produce a usable signature for the wrong origin. That&#39;s not &quot;harder to phish.&quot; It&#39;s structurally not phishable in the way a shared secret is.\u003C\u002Fp>",{"id":229,"html":230,"text":230,"type":147,"level":31},"b27","Two things to know before you reach for it",{"id":232,"type":171,"items":233,"ordered":18},"b28",[234,235],"\u003Cstrong>It needs a secure context and a stable \u003Ccode>rp.id\u003C\u002Fcode>.\u003C\u002Fstrong> WebAuthn only runs on HTTPS (or \u003Ccode>localhost\u003C\u002Fcode>), and \u003Ccode>rp.id\u003C\u002Fcode> has to be the exact domain or a registrable parent of it — a credential registered for \u003Ccode>app.example.com\u003C\u002Fcode> will not work if you later authenticate against a different subdomain unless \u003Ccode>rp.id\u003C\u002Fcode> was set to the shared parent domain from the start. Decide that ID before you have real users on it.","\u003Cstrong>You still store a public key and a sign counter, not &quot;nothing.&quot;\u003C\u002Fstrong> Losing your database doesn&#39;t hand out passwords — public keys are safe to leak by design — but you&#39;re still responsible for storing each credential&#39;s ID, public key, and signature counter correctly per user, and for letting people register a second authenticator in case they lose a device.",{"id":237,"html":238,"text":238,"type":147,"level":31},"b29","The takeaway",{"id":240,"html":241,"type":139},"b30","\u003Cp>A &quot;passkey&quot; isn&#39;t a separate technology from WebAuthn — it&#39;s WebAuthn with \u003Ccode>residentKey: &quot;required&quot;\u003C\u002Fcode> on registration and \u003Ccode>mediation: &quot;conditional&quot;\u003C\u002Fcode> on login, so the browser can offer the credential without you asking for a name first. Skip either one and you&#39;ve built a working, secure, entirely un-magical second factor that still makes people type their email.\u003C\u002Fp>",{"id":243,"html":244,"type":139},"b31","\u003Cp>Have you shipped a passkey flow that still shows a username field first? What did the fix turn out to be?\u003C\u002Fp>",{"id":246,"html":247,"type":139},"b32","\u003C!-- quiz:start -->",{"id":249,"html":250,"text":250,"type":147,"level":31},"b33","🧠 Test yourself",{"id":252,"html":253,"type":139},"b34","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fwebauthn-passkeys-discoverable-credentials\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":255,"html":256,"type":139},"b35","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":258,"html":259,"type":139},"b36","\u003C!-- quiz:end -->",{"id":261,"type":262},"b37","divider",{"id":264,"html":265,"type":139},"b38","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":267,"html":268,"type":139},"b39","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":270,"type":171,"items":271,"ordered":18},"b40",[272,273,274],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","I shipped \"Sign in with a passkey\" on a side project last month. Fingerprint reader popped up, the credential got created, login worked. I was pretty proud of it — right up until my partner tried it and typed her email address first, the way she does for every other login, and only *then* got the fingerprint prompt.\n\nThat's not a passkey. That's a security key with extra steps. The whole pitch of a passkey is that the browser already knows who's trying to sign in — no username field, no typing, just a tap. Mine still needed the name first. And the code I'd written looked exactly like the WebAuthn examples everyone links to.\n\n## The code that \"works\"\n\nHere's roughly what I had, trimmed to the part that matters:\n\n```js\nconst credential = await navigator.credentials.create({\n  publicKey: {\n    challenge: randomChallengeFromServer,\n    rp: { name: \"My App\", id: \"myapp.com\" },\n    user: {\n      id: userIdBytes,\n      name: \"you@example.com\",\n      displayName: \"You\",\n    },\n    pubKeyCredParams: [{ alg: -7, type: \"public-key\" }],\n  },\n});\n```\n\nThis is correct WebAuthn. It creates a real public\u002Fprivate key pair, stores the private half on the authenticator, and hands you back a public key to save on your server. Signing in with it later works fine — *if* you already know which user is signing in and can send back the specific `credential.id` you stored for them.\n\nThat \"if\" is the whole bug. Nothing here tells the authenticator to save this credential somewhere the browser can find *on its own*. So at login time, you're stuck asking for a username first, looking up that user's stored credential ID, and only then calling `navigator.credentials.get()` with it. Which is exactly the flow I'd built — technically WebAuthn, functionally a slower 2FA prompt.\n\n## Discoverable vs. non-discoverable — the distinction the tutorials skip\n\nA WebAuthn credential can be one of two things, and the spec's own names for them undersell how different they are in practice:\n\n- **Non-discoverable (a \"server-side\" credential).** The credential is registered, but nothing about it is stored *on the authenticator* in a way the browser can enumerate. To sign in, your server has to already know the user (from a username, a cookie, whatever) and hand back that user's specific credential ID in the `get()` call.\n- **Discoverable (what people mean by \"passkey\").** The credential itself is saved on the device — in the platform's keychain or the security key's own storage — with enough metadata that the browser can show it in a picker with **no prior input at all.** Type nothing, get a list of \"sign in as ___,\" tap one, done.\n\nSpot the fix yet? It's one field, in the options object you already have:\n\n```js\nconst credential = await navigator.credentials.create({\n  publicKey: {\n    challenge: randomChallengeFromServer,\n    rp: { name: \"My App\", id: \"myapp.com\" },\n    user: { id: userIdBytes, name: \"you@example.com\", displayName: \"You\" },\n    pubKeyCredParams: [{ alg: -7, type: \"public-key\" }],\n    authenticatorSelection: {\n      residentKey: \"required\",       \u002F\u002F ← this is the whole fix\n      userVerification: \"preferred\", \u002F\u002F ask for biometric\u002FPIN, don't require it\n    },\n  },\n});\n```\n\n`residentKey: \"required\"` tells the authenticator: don't just hand back a key, *keep a copy of yourself here* so the browser can find you later without being told your name. That's the difference between \"a WebAuthn credential\" and \"a passkey\" — the second is really just the first, stored discoverably.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fwebauthn-passkeys-discoverable-credentials\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The other half: how sign-in actually skips the username field\n\nMaking the credential discoverable only gets you halfway. The login call still has to *ask* for a discoverable credential instead of a specific known one — and there's a second option most people never see because it doesn't live in the WebAuthn spec at all. It's part of the separate Credential Management API, layered on top:\n\n```js\nconst credential = await navigator.credentials.get({\n  publicKey: {\n    challenge: randomChallengeFromServer,\n    \u002F\u002F no allowCredentials list — let the browser show whatever it has\n  },\n  mediation: \"conditional\",\n});\n```\n\n`mediation: \"conditional\"` is what makes a passkey show up **inside the browser's own autofill dropdown** on a plain `\u003Cinput autocomplete=\"username webauthn\">` field, sitting right next to any saved passwords — instead of requiring a separate \"Sign in with passkey\" button the user has to notice and click. Drop the button. Let the username field do it. That's the UX every passkey demo video is showing you, and it's a call that has nothing to do with `residentKey` — you need both.\n\n## Why this is more than a UX nitpick\n\nThe reason WebAuthn is worth this much ceremony isn't the fingerprint reader — Touch ID logins existed before this API. It's what happens underneath, on every sign-in:\n\n1. Your server sends a fresh, random **challenge**.\n2. The authenticator signs that challenge with the private key — the one that has never left the device and never will.\n3. Critically, the signature also covers the **origin** the browser is actually on. A credential created for `myapp.com` produces a signature that a server for `myapp.com` accepts — and a phishing page at `myapp-login.com` cannot get a valid one out of the same authenticator, because the browser includes the real origin, not whatever the page claims to be.\n\nPassword managers that autofill by domain get phished anyway when a user manually copy-pastes. WebAuthn doesn't give the user that option — there's no password to copy. The credential simply doesn't produce a usable signature for the wrong origin. That's not \"harder to phish.\" It's structurally not phishable in the way a shared secret is.\n\n## Two things to know before you reach for it\n\n- **It needs a secure context and a stable `rp.id`.** WebAuthn only runs on HTTPS (or `localhost`), and `rp.id` has to be the exact domain or a registrable parent of it — a credential registered for `app.example.com` will not work if you later authenticate against a different subdomain unless `rp.id` was set to the shared parent domain from the start. Decide that ID before you have real users on it.\n- **You still store a public key and a sign counter, not \"nothing.\"** Losing your database doesn't hand out passwords — public keys are safe to leak by design — but you're still responsible for storing each credential's ID, public key, and signature counter correctly per user, and for letting people register a second authenticator in case they lose a device.\n\n## The takeaway\n\nA \"passkey\" isn't a separate technology from WebAuthn — it's WebAuthn with `residentKey: \"required\"` on registration and `mediation: \"conditional\"` on login, so the browser can offer the credential without you asking for a name first. Skip either one and you've built a working, secure, entirely un-magical second factor that still makes people type their email.\n\nHave you shipped a passkey flow that still shows a username field first? What did the fix turn out to be?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fwebauthn-passkeys-discoverable-credentials\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":109,"canonical":277,"description":278},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fwebauthn-passkeys-discoverable-credentials","WebAuthn's own demo code creates a credential the browser can't find on its own — so your passkey button quietly turns into a fancier security key instead of an actual passwordless","01a07112-8d09-7178-b258-be8e65db83a0",{"id":281,"locked":18},"01a07112-8d2d-732a-a2fa-fe2c5263bda7",[283],{"id":45,"slug":46,"title":48,"_count":284},{"questions":51},[286],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":288,"questionCount":51},{"questions":51}]