{
    "componentChunkName": "component---src-pages-blog-markdown-remark-fields-slug-js",
    "path": "/blog/implement-passkeys-nextjs",
    "result": {"data":{"markdownRemark":{"html":"<p>Most passkey tutorials for Next.js stop where the real work begins. They show a dashboard toggle or a thin wrapper around <code class=\"language-text\">navigator.credentials</code>, then hand-wave the server side that actually decides whether a login is genuine. This guide does the opposite. It walks through a complete, self-hosted passkey flow in a Next.js App Router app, registration and authentication, using <a href=\"https://supertokens.com/\" target=\"_blank\" rel=\"nofollow\">SuperTokens</a> for the relying-party logic so no third-party identity provider ever holds the credentials.</p>\n<p>This post assumes the conceptual grounding from Post 1, What Are Passkeys? A Developer’s Guide to WebAuthn and FIDO2. The WebAuthn ceremony, origin binding, and public-key model are explained there and are not repeated here.</p>\n<h2 id=\"prerequisites\" style=\"position:relative;\"><a href=\"#prerequisites\" aria-label=\"prerequisites permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Prerequisites</h2>\n<p>Five things are needed before starting:</p>\n<ul>\n<li>A <a href=\"https://nextjs.org/\" target=\"_blank\" rel=\"nofollow\">Next.js</a> project using the App Router</li>\n<li>SuperTokens Node SDK installed (<a href=\"https://supertokens.com/docs/quickstart/backend-setup\" target=\"_blank\" rel=\"nofollow\"><code class=\"language-text\">supertokens-node</code></a> on the backend, <a href=\"https://supertokens.com/docs/quickstart/frontend-setup?uiType=custom\" target=\"_blank\" rel=\"nofollow\"><code class=\"language-text\">supertokens-web-js</code></a> on the frontend for custom UI)</li>\n<li>A SuperTokens Core instance (the hosted <code class=\"language-text\">try.supertokens.io</code> works for development; <a href=\"https://supertokens.com/docs/deployment/self-host-supertokens\" target=\"_blank\" rel=\"nofollow\">self-host</a> or <a href=\"https://supertokens.com/docs/quickstart/next-steps\" target=\"_blank\" rel=\"nofollow\">SuperTokens Cloud</a> for production)</li>\n<li>A database. Examples reference PostgreSQL, but the Core abstracts storage, so MySQL or the managed option work identically.</li>\n<li>Familiarity with the <a href=\"https://nextjs.org/docs/app\" target=\"_blank\" rel=\"nofollow\">App Router</a> and basic <a href=\"https://www.typescriptlang.org/docs/\" target=\"_blank\" rel=\"nofollow\">TypeScript</a></li>\n</ul>\n<h2 id=\"architecture-overview\" style=\"position:relative;\"><a href=\"#architecture-overview\" aria-label=\"architecture overview permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Architecture Overview</h2>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 630px; \"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/f76877d9de4fe1f506f892e74948c600/9af93/Architecture-Overview.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 65.18987341772153%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACA0lEQVQoz0WS627kIAxG828bLjbYQIAkkJB0Mp3ZVrujvv+zrTJTaaVPlrB0BPi4A2/OBHtmOKtmdNF/fN6O+3HcjuN2tUxCSamVVFJI8YpUsgM2NjJlT8mht5pAGg0W0ziOUx2nGnPWAP0JSA2amIjJklVadWgwrdN8b9NRrSMkY8hqA8S+LV9r/bLEUr2hQQ3akN3e98v1KLVo0B0YdMHHlNBAittlf7T9yLkYMqWWqYzODdv6GKdaysVYijlu++aDP29WoOu2fPy5t70R+3m+lGVv9cEc2tba1oiZOYU4WGKl9VzmtreY4wlrhJTTXMqyVuZYyrG9HzltIaayLHMpzntjwXmHBgHBkmXHluzz2QhoDSBoUEMoKTbnBzR2XMrf7+/b788wRCHFW//Wix4MoEUwaJ7ICTvv/OA5sGUqZZ/mpS2PeW3X++36cUs5n26UFEric0A+eOedRug06FjSuM9Dy0hWCNULgejZOXLMzr0ki6dhQ7audd3W+Zw2dFLJl/pTpPqx38tfQvXyyZy7odVrMZRW6nl8pXt1w5ymvdZLG+vkvEup1npb21GXLcTgQixla3vb3ve5Fhe8Av0fto58GkIcyDExsQthGNOYfzqOwxBTzjFl9oGcByRA273+00vRi74XvdLqnDxqjVqB1qDBIFprLAEyABvjjQnP6v8BSCg/OdxMUP4AAAAASUVORK5CYII='); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"Architecture-Overview\"\n        title=\"Architecture-Overview\"\n        src=\"/static/f76877d9de4fe1f506f892e74948c600/f058b/Architecture-Overview.png\"\n        srcset=\"/static/f76877d9de4fe1f506f892e74948c600/c26ae/Architecture-Overview.png 158w,\n/static/f76877d9de4fe1f506f892e74948c600/6bdcf/Architecture-Overview.png 315w,\n/static/f76877d9de4fe1f506f892e74948c600/f058b/Architecture-Overview.png 630w,\n/static/f76877d9de4fe1f506f892e74948c600/40601/Architecture-Overview.png 945w,\n/static/f76877d9de4fe1f506f892e74948c600/78612/Architecture-Overview.png 1260w,\n/static/f76877d9de4fe1f506f892e74948c600/9af93/Architecture-Overview.png 4800w\"\n        sizes=\"(max-width: 630px) 100vw, 630px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n        decoding=\"async\"\n      />\n  </a>\n    </span></p>\n<p>A passkey login involves three parties. The <strong>authenticator</strong> is the user’s device or security key, and it holds the private key. The <strong>relying party</strong> is the server that issues challenges and verifies signatures. The <strong>credential store</strong> is where the user’s public keys live. In a from-scratch build, all of the relying-party responsibility falls on application code: generating cryptographically random challenges, parsing and validating attestation objects, verifying signatures against stored public keys, tracking signature counters, and storing credentials safely.</p>\n<p>This is exactly where SuperTokens changes the shape of the work. In this architecture, SuperTokens Core is the relying party and the credential store. The Next.js app exposes SuperTokens’ authentication endpoints through a single route handler, the frontend calls SuperTokens SDK methods, and the Core performs challenge generation, attestation verification, and signature verification against the stored public key. Application code never touches an attestation object or a signature counter. That is the difference between wiring up a WebAuthn library by hand and standing on a maintained, open-source relying party that runs on infrastructure the team controls.</p>\n<p>The trade-off worth naming: rolling your own gives total control at the cost of owning every cryptographic edge case, and those edge cases are where WebAuthn implementations most often go subtly wrong. Delegating the ceremony to SuperTokens trades some of that low-level control for correctness that is already tested and maintained. For the large majority of teams shipping passkeys, that is the right trade.</p>\n<h2 id=\"project-setup-wiring-supertokens-into-nextjs\" style=\"position:relative;\"><a href=\"#project-setup-wiring-supertokens-into-nextjs\" aria-label=\"project setup wiring supertokens into nextjs permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Project Setup: Wiring SuperTokens Into Next.js</h2>\n<p>Three files connect SuperTokens to a Next.js App Router project: shared app info, the backend config, and the catch-all route handler that exposes the authentication APIs.</p>\n<p>Start with the shared configuration that both frontend and backend read. The <code class=\"language-text\">apiBasePath</code> of <code class=\"language-text\">/api/auth</code> is what makes the route handler below work, and the domains must match the actual origin the app runs on, which matters for passkeys specifically because credentials are origin-bound.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"72645347690369696000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`// config/appInfo.ts\nimport { AppInfo } from &quot;supertokens-node/types&quot;;\n\nexport const appInfo: AppInfo = {\n appName: &quot;Passkeys Demo&quot;,\n apiDomain: process.env.NEXT_PUBLIC_API_DOMAIN || &quot;http://localhost:3000&quot;,\n websiteDomain: process.env.NEXT_PUBLIC_WEBSITE_DOMAIN || &quot;http://localhost:3000&quot;,\n apiBasePath: &quot;/api/auth&quot;,\n websiteBasePath: &quot;/auth&quot;,\n};`, `72645347690369696000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token comment\">// config/appInfo.ts</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> AppInfo <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node/types\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> appInfo<span class=\"token operator\">:</span> AppInfo <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span>\n appName<span class=\"token operator\">:</span> <span class=\"token string\">\"Passkeys Demo\"</span><span class=\"token punctuation\">,</span>\n apiDomain<span class=\"token operator\">:</span> process<span class=\"token punctuation\">.</span>env<span class=\"token punctuation\">.</span><span class=\"token constant\">NEXT_PUBLIC_API_DOMAIN</span> <span class=\"token operator\">||</span> <span class=\"token string\">\"http://localhost:3000\"</span><span class=\"token punctuation\">,</span>\n websiteDomain<span class=\"token operator\">:</span> process<span class=\"token punctuation\">.</span>env<span class=\"token punctuation\">.</span><span class=\"token constant\">NEXT_PUBLIC_WEBSITE_DOMAIN</span> <span class=\"token operator\">||</span> <span class=\"token string\">\"http://localhost:3000\"</span><span class=\"token punctuation\">,</span>\n apiBasePath<span class=\"token operator\">:</span> <span class=\"token string\">\"/api/auth\"</span><span class=\"token punctuation\">,</span>\n websiteBasePath<span class=\"token operator\">:</span> <span class=\"token string\">\"/auth\"</span><span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Next, the backend config. Adding passkeys is a single line in the <code class=\"language-text\">recipeList</code>: <code class=\"language-text\">WebAuthn.init()</code>. The <code class=\"language-text\">Session.init()</code> recipe issues the session once a passkey login succeeds.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"80300122436923280000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`// config/backend.ts\nimport SuperTokens from &quot;supertokens-node&quot;;\nimport Session from &quot;supertokens-node/recipe/session&quot;;\nimport WebAuthn from &quot;supertokens-node/recipe/webauthn&quot;;\nimport { TypeInput } from &quot;supertokens-node/types&quot;;\nimport { appInfo } from &quot;./appInfo&quot;;\n\nexport const backendConfig = (): TypeInput => ({\n framework: &quot;custom&quot;,\n supertokens: {\n   connectionURI: process.env.SUPERTOKENS_CONNECTION_URI || &quot;https://try.supertokens.io&quot;,\n   // apiKey: process.env.SUPERTOKENS_API_KEY, // set this for a self-hosted or cloud Core\n },\n appInfo,\n recipeList: [\n   WebAuthn.init(),\n   Session.init(),\n ],\n isInServerlessEnv: true,\n});\n\nlet initialized = false;\n\nexport function ensureSuperTokensInit() {\n if (!initialized) {\n   SuperTokens.init(backendConfig());\n   initialized = true;\n }\n}`, `80300122436923280000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token comment\">// config/backend.ts</span>\n<span class=\"token keyword\">import</span> SuperTokens <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> Session <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node/recipe/session\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> WebAuthn <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node/recipe/webauthn\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> TypeInput <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node/types\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> appInfo <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"./appInfo\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> backendConfig <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> TypeInput <span class=\"token operator\">=></span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n framework<span class=\"token operator\">:</span> <span class=\"token string\">\"custom\"</span><span class=\"token punctuation\">,</span>\n supertokens<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n   connectionURI<span class=\"token operator\">:</span> process<span class=\"token punctuation\">.</span>env<span class=\"token punctuation\">.</span><span class=\"token constant\">SUPERTOKENS_CONNECTION_URI</span> <span class=\"token operator\">||</span> <span class=\"token string\">\"https://try.supertokens.io\"</span><span class=\"token punctuation\">,</span>\n   <span class=\"token comment\">// apiKey: process.env.SUPERTOKENS_API_KEY, // set this for a self-hosted or cloud Core</span>\n <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n appInfo<span class=\"token punctuation\">,</span>\n recipeList<span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n   WebAuthn<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n   Session<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n <span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n isInServerlessEnv<span class=\"token operator\">:</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">let</span> initialized <span class=\"token operator\">=</span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">ensureSuperTokensInit</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>initialized<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n   SuperTokens<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token function\">backendConfig</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n   initialized <span class=\"token operator\">=</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">;</span>\n <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Finally, the catch-all route handler. This single file exposes every SuperTokens authentication endpoint, including all the <code class=\"language-text\">webauthn/*</code> routes, under <code class=\"language-text\">/api/auth</code>. It proxies each HTTP method to the SuperTokens handler.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"3455608929146247700\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`// app/api/auth/[[...path]]/route.ts\nimport { getAppDirRequestHandler } from &quot;supertokens-node/nextjs&quot;;\nimport { NextRequest } from &quot;next/server&quot;;\nimport { ensureSuperTokensInit } from &quot;../../../../config/backend&quot;;\n\nensureSuperTokensInit();\n\nconst handleCall = getAppDirRequestHandler();\n\nexport async function GET(request: NextRequest) {\n const response = await handleCall(request);\n if (!response.headers.has(&quot;Cache-Control&quot;)) {\n   response.headers.set(\n     &quot;Cache-Control&quot;,\n     &quot;no-cache, no-store, max-age=0, must-revalidate&quot;\n   );\n }\n return response;\n}\n\nexport async function POST(request: NextRequest) {\n return handleCall(request);\n}\n\nexport const DELETE = POST;\nexport const PUT = POST;\nexport const PATCH = POST;\nexport const HEAD = GET;`, `3455608929146247700`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token comment\">// app/api/auth/[[...path]]/route.ts</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> getAppDirRequestHandler <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node/nextjs\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> NextRequest <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"next/server\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> ensureSuperTokensInit <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"../../../../config/backend\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token function\">ensureSuperTokensInit</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">const</span> handleCall <span class=\"token operator\">=</span> <span class=\"token function\">getAppDirRequestHandler</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token constant\">GET</span><span class=\"token punctuation\">(</span>request<span class=\"token operator\">:</span> NextRequest<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n <span class=\"token keyword\">const</span> response <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">handleCall</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>response<span class=\"token punctuation\">.</span>headers<span class=\"token punctuation\">.</span><span class=\"token function\">has</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Cache-Control\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n   response<span class=\"token punctuation\">.</span>headers<span class=\"token punctuation\">.</span><span class=\"token function\">set</span><span class=\"token punctuation\">(</span>\n     <span class=\"token string\">\"Cache-Control\"</span><span class=\"token punctuation\">,</span>\n     <span class=\"token string\">\"no-cache, no-store, max-age=0, must-revalidate\"</span>\n   <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n <span class=\"token punctuation\">}</span>\n <span class=\"token keyword\">return</span> response<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token constant\">POST</span><span class=\"token punctuation\">(</span>request<span class=\"token operator\">:</span> NextRequest<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n <span class=\"token keyword\">return</span> <span class=\"token function\">handleCall</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> <span class=\"token constant\">DELETE</span> <span class=\"token operator\">=</span> <span class=\"token constant\">POST</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> <span class=\"token constant\">PUT</span> <span class=\"token operator\">=</span> <span class=\"token constant\">POST</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> <span class=\"token constant\">PATCH</span> <span class=\"token operator\">=</span> <span class=\"token constant\">POST</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> <span class=\"token constant\">HEAD</span> <span class=\"token operator\">=</span> <span class=\"token constant\">GET</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>On the frontend, initialize the web SDK for custom UI. This is what exposes the passkey helper methods to the browser.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"57896927813794720000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`// config/frontend.ts\nimport SuperTokensWebJs from &quot;supertokens-web-js&quot;;\nimport Session from &quot;supertokens-web-js/recipe/session&quot;;\nimport WebAuthn from &quot;supertokens-web-js/recipe/webauthn&quot;;\nimport { appInfo } from &quot;./appInfo&quot;;\n\nlet initialized = false;\n\nexport function ensureSuperTokensFrontendInit() {\n if (!initialized) {\n   SuperTokensWebJs.init({\n     appInfo,\n     recipeList: [Session.init(), WebAuthn.init()],\n   });\n   initialized = true;\n }\n}`, `57896927813794720000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token comment\">// config/frontend.ts</span>\n<span class=\"token keyword\">import</span> SuperTokensWebJs <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-web-js\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> Session <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-web-js/recipe/session\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> WebAuthn <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-web-js/recipe/webauthn\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> appInfo <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"./appInfo\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">let</span> initialized <span class=\"token operator\">=</span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">ensureSuperTokensFrontendInit</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>initialized<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n   SuperTokensWebJs<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n     appInfo<span class=\"token punctuation\">,</span>\n     recipeList<span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>Session<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> WebAuthn<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n   <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n   initialized <span class=\"token operator\">=</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">;</span>\n <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>With those in place, the authentication flows themselves are short, because the ceremony lives inside the SDK and the Core.</p>\n<h2 id=\"the-registration-flow\" style=\"position:relative;\"><a href=\"#the-registration-flow\" aria-label=\"the registration flow permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Registration Flow</h2>\n<p>Registration creates a new passkey and links it to a new user account. On the client, a single SuperTokens method drives the whole ceremony. <code class=\"language-text\">registerCredentialWithSignUp</code> fetches the registration options from the backend, invokes the browser’s WebAuthn API to create the key pair, and submits the resulting credential back for verification. The component below is the full, copyable registration UI.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"53262032042856420000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`// app/auth/PasskeyAuth.tsx\n&quot;use client&quot;;\n\nimport { useEffect, useState } from &quot;react&quot;;\nimport {\n registerCredentialWithSignUp,\n authenticateCredentialWithSignIn,\n} from &quot;supertokens-web-js/recipe/webauthn&quot;;\nimport { ensureSuperTokensFrontendInit } from &quot;../../config/frontend&quot;;\n\nexport default function PasskeyAuth() {\n const [email, setEmail] = useState(&quot;&quot;);\n const [message, setMessage] = useState(&quot;&quot;);\n\n useEffect(() => {\n   ensureSuperTokensFrontendInit();\n }, []);\n\n async function handleSignUp() {\n   try {\n     const response = await registerCredentialWithSignUp({ email });\n\n     if (response.status === &quot;OK&quot;) {\n       setMessage(&quot;Passkey created. You are signed up and logged in.&quot;);\n     } else if (response.status === &quot;INVALID_EMAIL_ERROR&quot;) {\n       setMessage(&quot;Please enter a valid email address.&quot;);\n     } else if (response.status === &quot;EMAIL_ALREADY_EXISTS_ERROR&quot;) {\n       setMessage(&quot;That email already has an account. Try signing in instead.&quot;);\n     } else if (response.status === &quot;WEBAUTHN_NOT_SUPPORTED&quot;) {\n       setMessage(&quot;This browser or device does not support passkeys.&quot;);\n     } else if (response.status === &quot;SIGN_UP_NOT_ALLOWED&quot;) {\n       // response.reason is a user-friendly message, sometimes with a support code\n       setMessage(response.reason);\n     } else if (response.status === &quot;AUTHENTICATOR_ALREADY_REGISTERED&quot;) {\n       setMessage(&quot;This passkey is already registered to an account.&quot;);\n     } else {\n       // INVALID_CREDENTIALS_ERROR, OPTIONS_NOT_FOUND_ERROR,\n       // INVALID_OPTIONS_ERROR, FAILED_TO_REGISTER_USER, etc.\n       setMessage(&quot;Could not create a passkey. Please try again.&quot;);\n     }\n   } catch (err: any) {\n     if (err.isSuperTokensGeneralError === true) {\n       setMessage(err.message);\n     } else {\n       setMessage(&quot;Something went wrong. Please try again.&quot;);\n     }\n   }\n }\n\n async function handleSignIn() {\n   try {\n     const response = await authenticateCredentialWithSignIn();\n\n     if (response.status === &quot;OK&quot;) {\n       setMessage(&quot;Signed in with your passkey.&quot;);\n     } else if (response.status === &quot;WEBAUTHN_NOT_SUPPORTED&quot;) {\n       setMessage(&quot;This browser or device does not support passkeys.&quot;);\n     } else if (response.status === &quot;SIGN_IN_NOT_ALLOWED&quot;) {\n       setMessage(response.reason);\n     } else {\n       // INVALID_CREDENTIALS_ERROR, FAILED_TO_AUTHENTICATE_USER\n       setMessage(&quot;Could not sign in. Please try again.&quot;);\n     }\n   } catch (err: any) {\n     if (err.isSuperTokensGeneralError === true) {\n       setMessage(err.message);\n     } else {\n       setMessage(&quot;Something went wrong. Please try again.&quot;);\n     }\n   }\n }\n\n return (\n   <div>\n     <input\n       type=&quot;email&quot;\n       value={email}\n       onChange={(e) => setEmail(e.target.value)}\n       placeholder=&quot;you@example.com&quot;\n     />\n     <button onClick={handleSignUp}>Create account with passkey</button>\n     <button onClick={handleSignIn}>Sign in with passkey</button>\n     <p role=&quot;status&quot;>{message}</p>\n   </div>\n );\n}`, `53262032042856420000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token comment\">// app/auth/PasskeyAuth.tsx</span>\n<span class=\"token string\">\"use client\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> useEffect<span class=\"token punctuation\">,</span> useState <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"react\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span>\n registerCredentialWithSignUp<span class=\"token punctuation\">,</span>\n authenticateCredentialWithSignIn<span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-web-js/recipe/webauthn\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> ensureSuperTokensFrontendInit <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"../../config/frontend\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">default</span> <span class=\"token keyword\">function</span> <span class=\"token function\">PasskeyAuth</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n <span class=\"token keyword\">const</span> <span class=\"token punctuation\">[</span>email<span class=\"token punctuation\">,</span> setEmail<span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token function\">useState</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n <span class=\"token keyword\">const</span> <span class=\"token punctuation\">[</span>message<span class=\"token punctuation\">,</span> setMessage<span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token function\">useState</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n <span class=\"token function\">useEffect</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n   <span class=\"token function\">ensureSuperTokensFrontendInit</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">handleSignUp</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n   <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n     <span class=\"token keyword\">const</span> response <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">registerCredentialWithSignUp</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> email <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n     <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"OK\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Passkey created. You are signed up and logged in.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"INVALID_EMAIL_ERROR\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Please enter a valid email address.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"EMAIL_ALREADY_EXISTS_ERROR\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"That email already has an account. Try signing in instead.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"WEBAUTHN_NOT_SUPPORTED\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"This browser or device does not support passkeys.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"SIGN_UP_NOT_ALLOWED\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token comment\">// response.reason is a user-friendly message, sometimes with a support code</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>reason<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"AUTHENTICATOR_ALREADY_REGISTERED\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"This passkey is already registered to an account.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token comment\">// INVALID_CREDENTIALS_ERROR, OPTIONS_NOT_FOUND_ERROR,</span>\n       <span class=\"token comment\">// INVALID_OPTIONS_ERROR, FAILED_TO_REGISTER_USER, etc.</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Could not create a passkey. Please try again.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span>\n   <span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span>err<span class=\"token operator\">:</span> <span class=\"token builtin\">any</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n     <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>err<span class=\"token punctuation\">.</span>isSuperTokensGeneralError <span class=\"token operator\">===</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span>err<span class=\"token punctuation\">.</span>message<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Something went wrong. Please try again.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span>\n   <span class=\"token punctuation\">}</span>\n <span class=\"token punctuation\">}</span>\n\n <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">handleSignIn</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n   <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n     <span class=\"token keyword\">const</span> response <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">authenticateCredentialWithSignIn</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n     <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"OK\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Signed in with your passkey.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"WEBAUTHN_NOT_SUPPORTED\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"This browser or device does not support passkeys.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>status <span class=\"token operator\">===</span> <span class=\"token string\">\"SIGN_IN_NOT_ALLOWED\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span>response<span class=\"token punctuation\">.</span>reason<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token comment\">// INVALID_CREDENTIALS_ERROR, FAILED_TO_AUTHENTICATE_USER</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Could not sign in. Please try again.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span>\n   <span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span>err<span class=\"token operator\">:</span> <span class=\"token builtin\">any</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n     <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>err<span class=\"token punctuation\">.</span>isSuperTokensGeneralError <span class=\"token operator\">===</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span>err<span class=\"token punctuation\">.</span>message<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\n       <span class=\"token function\">setMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Something went wrong. Please try again.\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n     <span class=\"token punctuation\">}</span>\n   <span class=\"token punctuation\">}</span>\n <span class=\"token punctuation\">}</span>\n\n <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\n   <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n     </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>input</span>\n       <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>email<span class=\"token punctuation\">\"</span></span>\n       <span class=\"token attr-name\">value</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span>email<span class=\"token punctuation\">}</span></span>\n       <span class=\"token attr-name\">onChange</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">e</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token function\">setEmail</span><span class=\"token punctuation\">(</span>e<span class=\"token punctuation\">.</span>target<span class=\"token punctuation\">.</span>value<span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span></span>\n       <span class=\"token attr-name\">placeholder</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>you@example.com<span class=\"token punctuation\">\"</span></span>\n     <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n     </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>button</span> <span class=\"token attr-name\">onClick</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span>handleSignUp<span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Create account with passkey</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n     </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>button</span> <span class=\"token attr-name\">onClick</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span>handleSignIn<span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Sign in with passkey</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n     </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span> <span class=\"token attr-name\">role</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>status<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token punctuation\">{</span>message<span class=\"token punctuation\">}</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n   </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<h3 id=\"under-the-hood\" style=\"position:relative;\"><a href=\"#under-the-hood\" aria-label=\"under the hood permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a><strong>Under The Hood</strong></h3>\n<p>The single <code class=\"language-text\">registerCredentialWithSignUp</code> call is not magic, and understanding what it does is what separates a working integration from a mysterious one. It runs the exact WebAuthn ceremony from Post 1, mapped onto three SuperTokens endpoints exposed by the route handler:</p>\n<ul>\n<li>It POSTs to <code class=\"language-text\">/api/auth/webauthn/register/options</code> with the email. SuperTokens Core generates a random challenge and returns the registration options, including a <code class=\"language-text\">webauthnGeneratedOptionsId</code> that ties the rest of the flow to that specific challenge.</li>\n<li>It passes those options to the browser’s <code class=\"language-text\">navigator.credentials.create()</code>, which prompts the user for a biometric or PIN and generates the key pair in secure hardware.</li>\n<li>It POSTs the new credential to <code class=\"language-text\">/api/auth/webauthn/signup</code>, sending the <code class=\"language-text\">webauthnGeneratedOptionsId</code> alongside the credential’s <code class=\"language-text\">clientDataJSON</code> and <code class=\"language-text\">attestationObject</code>. The Core verifies the attestation, stores the public key, creates the user, and issues a session.</li>\n</ul>\n<p>The reason to use the SDK method rather than calling those endpoints by hand is serialization. WebAuthn passes binary data, and every field crossing the browser boundary has to be base64url-encoded and decoded precisely. The SDK handles that encoding, which is one of the most common sources of silent failures in hand-rolled implementations. Teams that need full control, for a non-web platform or a heavily customized UI, can call the three endpoints directly, but for a Next.js web app the helper is both correct and complete.</p>\n<h3 id=\"common-pitfalls\" style=\"position:relative;\"><a href=\"#common-pitfalls\" aria-label=\"common pitfalls permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a><strong>Common Pitfalls</strong></h3>\n<p>Two misconfigurations account for most first-run failures, and both trace back to origin binding. The first is an <strong>origin mismatch</strong>: the <code class=\"language-text\">apiDomain</code> and <code class=\"language-text\">websiteDomain</code> in <code class=\"language-text\">appInfo</code> must match the actual URL the browser loads. A passkey created while the app runs on <code class=\"language-text\">localhost:3000</code> will not verify if the request appears to originate from <code class=\"language-text\">127.0.0.1:3000</code>, because the browser treats those as different origins. The second is a <strong>relying-party ID misconfiguration</strong>: the RP ID is the registrable domain, with no scheme and no port, so <code class=\"language-text\">example.com</code>, not <code class=\"language-text\">https://example.com:3000</code>. A passkey is permanently bound to the RP ID it was created under, and even a mismatched subdomain can cause a silent failure where the authenticator simply declines to produce a credential with no obvious error. When a passkey flow fails quietly in production but works in development, the RP ID and origin configuration is the first place to look.</p>\n<h2 id=\"the-authentication-flow\" style=\"position:relative;\"><a href=\"#the-authentication-flow\" aria-label=\"the authentication flow permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Authentication Flow</h2>\n<p>Authentication proves an existing user with a passkey they already registered. The client side is the <code class=\"language-text\">handleSignIn</code> function already shown in the component above, driven by <code class=\"language-text\">authenticateCredentialWithSignIn</code>. Notice it takes no email argument. Passkeys registered as discoverable credentials let the authenticator present the available accounts for the site directly, so the user picks their passkey from the browser or OS prompt rather than typing an identifier first.</p>\n<h3 id=\"under-the-hood-1\" style=\"position:relative;\"><a href=\"#under-the-hood-1\" aria-label=\"under the hood 1 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a><strong>Under The Hood</strong></h3>\n<p>Authentication mirrors registration across two endpoints:</p>\n<ul>\n<li><code class=\"language-text\">authenticateCredentialWithSignIn</code> POSTs to <code class=\"language-text\">/api/auth/webauthn/signin/options</code>, and SuperTokens Core returns a fresh random challenge plus a webauthnGeneratedOptionsId.</li>\n<li>It passes the options to the browser’s <code class=\"language-text\">navigator.credentials.get()</code>, which prompts the user and signs the challenge with the private key held in secure hardware.</li>\n<li>It POSTs the signed assertion to <code class=\"language-text\">/api/auth/webauthn/signin</code>, sending the <code class=\"language-text\">clientDataJSON</code>, <code class=\"language-text\">authenticatorData</code>, and <code class=\"language-text\">signature</code>. SuperTokens Core verifies that signature against the public key it stored at registration, and on success issues a session.</li>\n</ul>\n<p>The private key never leaves the device in either flow. Registration transmits a public key once, and every authentication transmits only a signature over a one-time challenge, which is what makes the flow phishing-resistant rather than merely passwordless.</p>\n<p>Once a session exists, protecting routes uses the standard SuperTokens session verification. On the client, <code class=\"language-text\">Session.doesSessionExist()</code> from <code class=\"language-text\">supertokens-web-js/recipe/session</code> gates UI, and on the server, session verification through the SuperTokens Node SDK gates data. Session handling is shared across all SuperTokens recipes and is not specific to passkeys, so an existing SuperTokens app gains passkey login without changing how sessions work.</p>\n<h2 id=\"cross-device-passkeys\" style=\"position:relative;\"><a href=\"#cross-device-passkeys\" aria-label=\"cross device passkeys permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Cross-Device Passkeys</h2>\n<p>A user often registers a passkey on one device and later signs in on another. What happens next depends entirely on the passkey type covered in Post 1. A <strong>synced passkey</strong>, stored in iCloud Keychain, Google Password Manager, or a Windows-linked account, propagates to the user’s other devices automatically, so authentication on a second device just works with no extra code. A <strong>device-bound passkey</strong> on a hardware key does not sync, and authenticating on a new device uses the cross-device hybrid flow, where the user scans a QR code with the phone holding the passkey and approves the login over a proximity channel.</p>\n<p>From the application’s perspective, none of this requires special handling as long as the RP ID is configured correctly, because the browser and operating system manage the transport. The one requirement is that the RP ID stays consistent across every surface the app runs on, since a passkey bound to example.com will not present itself on a different registrable domain. A full treatment of cross-device edge cases and conditional UI autofill is a candidate for a dedicated future post, but this is enough that a cross-device login will not come as a surprise.</p>\n<h2 id=\"recovery-flow-considerations\" style=\"position:relative;\"><a href=\"#recovery-flow-considerations\" aria-label=\"recovery flow considerations permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Recovery Flow Considerations</h2>\n<p>This is the section that competitor guides consistently flag as the most commonly botched part of a passkey rollout, and the advice is blunt: do not ship passkeys without deciding this first. A passkey login is only as reliable as its recovery path, and the failure mode is severe. If a user’s only passkey lives on a single device and that device is lost, and no fallback exists, that account is permanently locked out.</p>\n<p>The design question to answer before launch is what happens when a user has no working authenticator. Three approaches cover most products. The first is a <strong>fallback authentication factor</strong>: keep email one-time codes or a password available as an alternate way in, so a lost device is an inconvenience rather than a lockout. The second is a <strong>recovery token flow</strong>: SuperTokens’ WebAuthn recipe exposes account recovery token generation, which can email a user a time-limited link that lets them register a new passkey after proving control of their email. The third is <strong>encouraging multiple passkeys</strong> at enrollment, prompting users to add a second authenticator such as a hardware key, so the loss of one device never removes their only credential.</p>\n<p>The mistake to avoid is treating recovery as a later problem. A passkey system with no recovery design is not a minimal version of a passkey system, it is a lockout generator waiting for its first lost phone. Decide the fallback, build it alongside the happy path, and test the lost-device scenario before any real user hits it.</p>\n<h2 id=\"migration-adding-passkeys-alongside-existing-password-auth\" style=\"position:relative;\"><a href=\"#migration-adding-passkeys-alongside-existing-password-auth\" aria-label=\"migration adding passkeys alongside existing password auth permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Migration: Adding Passkeys Alongside Existing Password Auth</h2>\n<p>Many teams do not want to replace passwords, they want to offer passkeys as an additional, better option. SuperTokens supports this directly, because authentication methods are modular recipes. An app already running <code class=\"language-text\">EmailPassword.init()</code> adds passkeys by including <code class=\"language-text\">WebAuthn.init()</code> in the same <code class=\"language-text\">recipeList</code>, with no removal of the existing flow.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"46967058085056350000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`recipeList: [\n EmailPassword.init(),\n WebAuthn.init(),\n Session.init(),\n],`, `46967058085056350000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\">recipeList<span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n EmailPassword<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n WebAuthn<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n Session<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span></code></pre></div>\n<p>With both recipes active, existing users keep signing in with their password while new and returning users can enroll a passkey as an alternative. SuperTokens’ account linking ties multiple login methods to a single user identity, so a user who signs up with a password and later adds a passkey remains one account rather than two. This is the low-risk migration path: offer passkeys alongside passwords, let adoption grow, and treat passwords as the fallback factor the recovery section calls for rather than deprecating them on day one. Teams arriving from another provider, following a Firebase Authentication or WorkOS migration guide, can layer passkeys on during the same move.</p>\n<h2 id=\"the-full-working-code-sample\" style=\"position:relative;\"><a href=\"#the-full-working-code-sample\" aria-label=\"the full working code sample permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Full Working Code Sample</h2>\n<p>The snippets above are the complete flow, but a runnable reference is faster to start from. The quickest way to a working passkeys project is the SuperTokens scaffolding tool, which generates a full App Router app with the recipe already wired:</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"28007348991928627000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`npx create-supertokens-app@latest`, `28007348991928627000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">npx create-supertokens-app@latest</code></pre></div>\n<p>Select the App Router and the passkeys option when prompted, and the generated project contains the backend config, the route handler, and a working custom UI equivalent to what this post builds. [A companion repository with the exact code from this tutorial is linked here.] It contains the full Next.js project, environment variable setup, and both the SDK-based flow and the raw-endpoint version for readers who want to see the ceremony wired by hand.</p>\n<h2 id=\"closing\" style=\"position:relative;\"><a href=\"#closing\" aria-label=\"closing permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Closing</h2>\n<p>That is a complete, self-hosted passkey implementation in Next.js: registration, authentication, cross-device behavior, a recovery design, and a migration path, with SuperTokens acting as the relying party so the credentials never leave infrastructure the team controls. For teams still weighing whether passkeys are the right call for their product, Post 3 in this series compares passkeys against the alternatives and lays out where they fit and where they do not. For the broader picture of structuring authentication in a Next.js App Router app, the Next.js App Router authentication guide covers session handling, route protection, and server-side verification end to end. SuperTokens supports passkeys as one recipe in an open-source, self-hostable authentication stack, which is what lets the entire flow above run without handing any part of it to a third party.</p>","frontmatter":{"date":"August 23, 2026","updated":null,"title":"How to Implement Passkey Authentication in Next.js With SuperTokens","cover":"implement-passkeys-nextjs.png","author":"Mostafa Ibrahim","description":"Build a complete, self-hosted passkey flow in a Next.js App Router app with SuperTokens: registration, authentication, cross-device, recovery, and migration."},"fields":{"slug":"/implement-passkeys-nextjs/"}},"site":{"siteMetadata":{"title":"SuperTokens Blog"}}},"pageContext":{"id":"201294b0-0b5f-5dfe-81e0-d88baf932ce6","fields__slug":"/implement-passkeys-nextjs/","__params":{"fields__slug":"implement-passkeys-nextjs"}}},
    "staticQueryHashes": []}