<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Passlock | Blog</title><description>Passkey authentication for Lucia, SvelteKit and other frameworks</description><link>https://passlock.dev/</link><language>en</language><item><title>Ensure your LLM coding agent uses the latest Passlock docs</title><link>https://passlock.dev/blog/ai-agent-skill/</link><guid isPermaLink="true">https://passlock.dev/blog/ai-agent-skill/</guid><description>The Passlock agent skill helps AI coding agents use the current public docs, LLM runbooks, and API references instead of relying on stale training data. Install the skill once, then ask your agent to use it when building with Passlock.

</description><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Passlock now provides an &lt;strong&gt;AI agent skill&lt;/strong&gt; for Codex, Claude Code, and other LLM coding agents.&lt;/p&gt;
&lt;p&gt;The skill is a small &lt;code dir=&quot;auto&quot;&gt;SKILL.md&lt;/code&gt; file that tells an agent how to discover the current Passlock documentation, search the agent-friendly runbooks, and fetch only the pages that are relevant to the task.&lt;/p&gt;
&lt;p&gt;If you use AI coding tools to add passkeys, one-time codes, or Passlock server flows, install the skill before asking the agent to write code. It gives the agent a reliable path to the latest docs instead of leaving it to guess from stale model knowledge.&lt;/p&gt;
&lt;p&gt;See the full setup reference at &lt;a href=&quot;https://passlock.dev/agents/agent-skill/&quot;&gt;passlock.dev/agents/agent-skill/&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-we-added-an-agent-skill&quot;&gt;Why we added an agent skill&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Passlock’s public API is intentionally small, but passkey integration still has details that matter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;browser and server packages have different responsibilities&lt;/li&gt;
&lt;li&gt;safe and unsafe entry points return errors differently&lt;/li&gt;
&lt;li&gt;passkey registration and authentication require separate browser and server steps&lt;/li&gt;
&lt;li&gt;REST API endpoints use tenancy-scoped URLs&lt;/li&gt;
&lt;li&gt;API symbols and result types can change as the libraries evolve&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those details are exactly where an LLM can make plausible but incorrect assumptions.&lt;/p&gt;
&lt;p&gt;The agent skill narrows that risk. It instructs the agent to start from Passlock’s public documentation, use the generated LLM indexes for search, and treat the hosted API reference as the source of truth for exact package symbols.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-skill-does&quot;&gt;What the skill does&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The skill teaches an agent a retrieval workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;start with &lt;code dir=&quot;auto&quot;&gt;https://passlock.dev/llms.txt&lt;/code&gt; for broad documentation discovery&lt;/li&gt;
&lt;li&gt;search the generated LLM documentation index for topic-specific pages&lt;/li&gt;
&lt;li&gt;use &lt;code dir=&quot;auto&quot;&gt;https://apidocs.passlock.dev/llms/&lt;/code&gt; for symbol-level package reference&lt;/li&gt;
&lt;li&gt;prefer runbook pages for integration flow and API docs for exact signatures&lt;/li&gt;
&lt;li&gt;fetch only the relevant markdown pages after the search has narrowed the topic&lt;/li&gt;
&lt;li&gt;cite public Passlock URLs when explaining the implementation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It does not give the agent private implementation details. It points the agent at the same public documentation you can read on the website.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-to-install-it&quot;&gt;Where to install it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Download the skill from the reference page and place it where your coding agent expects skills or project instructions.&lt;/p&gt;
&lt;p&gt;For Codex project-level use:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.agents/skills/passlock-agent-docs/SKILL.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For Codex user-level use:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;~/.agents/skills/passlock-agent-docs/SKILL.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For Claude Code project-level use:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.claude/skills/passlock-agent-docs/SKILL.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For Claude Code user-level use:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;~/.claude/skills/passlock-agent-docs/SKILL.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Use a project-level install when the repository actively integrates Passlock. Use a user-level install when you want the skill available across many projects.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-to-use-it&quot;&gt;How to use it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;After installing the skill, ask your agent to use it when working on Passlock code:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Use the Passlock agent docs skill to add passkey registration to this app.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Or make the retrieval target explicit:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Use the Passlock agent docs skill. Verify the current @passlock/browser API before changing this registration flow.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For server-side work, point the agent at the package boundary:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Use the Passlock agent docs skill and update this API route to exchange a Passlock code with @passlock/server.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Good prompts tell the agent which Passlock flow is being changed and require it to verify the current docs before editing code.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;when-it-helps-most&quot;&gt;When it helps most&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The skill is most useful when the agent needs to choose between similar Passlock concepts:&lt;/p&gt;
&lt;p&gt;| Task | Why the skill helps |
|:—|:—|
| Adding passkey registration | Finds the browser registration flow and the server code exchange flow |
| Handling expected errors | Confirms whether the safe or unsafe entry point matches the app style |
| Updating an existing integration | Checks current symbols before editing older code |
| Calling the REST API directly | Finds endpoint-specific request and response details |
| Explaining Passlock behavior | Uses public citations instead of unsupported guesses |&lt;/p&gt;
&lt;p&gt;The default recommendation is to use the safe Passlock APIs unless your application already standardizes on thrown errors. The skill should guide the agent toward those safe examples for runbook-style implementation work.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;keep-the-agent-honest&quot;&gt;Keep the agent honest&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The skill improves retrieval, but it is still worth making your prompt specific. Ask the agent to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;verify the relevant Passlock docs before editing&lt;/li&gt;
&lt;li&gt;prefer public runbooks for flow guidance&lt;/li&gt;
&lt;li&gt;use the API reference for exact imports and type names&lt;/li&gt;
&lt;li&gt;avoid inventing package APIs&lt;/li&gt;
&lt;li&gt;run your project’s normal typecheck or build after changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That combination gives the agent enough context to move quickly without treating generated code as authoritative.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/agents/agent-skill/&quot;&gt;Passlock skill for Codex, Claude and other LLM coding agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/llms.txt&quot;&gt;Passlock LLM documentation index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apidocs.passlock.dev/llms/browser/README.md&quot;&gt;@passlock/browser API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apidocs.passlock.dev/llms/server/README.md&quot;&gt;@passlock/server API reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Class based clients and tree-shakeable functions</title><link>https://passlock.dev/blog/class-and-function-entrypoints/</link><guid isPermaLink="true">https://passlock.dev/blog/class-and-function-entrypoints/</guid><description>Passlock&apos;s browser and server libraries now expose the same capabilities through class-based clients and standalone functions. Choose class clients when you want config in one place, or standalone functions when bundle size and tree-shaking matter most.

</description><pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;span&gt;New feature&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Passlock now supports both &lt;strong&gt;class-based clients&lt;/strong&gt; and &lt;strong&gt;standalone functions&lt;/strong&gt; across the browser and server libraries.&lt;/p&gt;
&lt;p&gt;Both styles are functionally equivalent. The difference is how you want to structure your integration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use a &lt;code dir=&quot;auto&quot;&gt;Passlock&lt;/code&gt; class when you want to configure the client once&lt;/li&gt;
&lt;li&gt;use standalone functions when you want tree-shakeable imports&lt;/li&gt;
&lt;li&gt;use safe entry points when expected Passlock errors should return as typed result envelopes&lt;/li&gt;
&lt;li&gt;use unsafe entry points when expected Passlock errors should throw and be handled with &lt;code dir=&quot;auto&quot;&gt;try&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;catch&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the full side-by-side reference, see &lt;a href=&quot;https://passlock.dev/style/&quot;&gt;Choose your code style&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-we-added-both-styles&quot;&gt;Why we added both styles&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Different teams want different integration shapes.&lt;/p&gt;
&lt;p&gt;Some applications prefer a configured client object. That keeps tenancy and API configuration in one place, makes dependency injection straightforward, and reads naturally in service classes or route handlers.&lt;/p&gt;
&lt;p&gt;Other applications care more about importing only the functions they use. That is especially useful in browser code, where a registration-only page should not have to pull in every other client method just because a class exists.&lt;/p&gt;
&lt;p&gt;Passlock now supports both approaches directly, without forcing either code style on every project.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;class-clients&quot;&gt;Class clients&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Class clients are the simplest option when you call Passlock from several places that share the same config.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Passlock } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/browser&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;passlock&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Passlock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;passlock&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;registerPasskey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;username: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;jdoe@example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The same pattern works in server code:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Passlock } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/server&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;passlock&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Passlock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;apiKey: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myApiKey&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;passlock&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exchangeCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The class methods supply the constructor config for every operation. That reduces repetition and keeps application-level setup separate from per-call data.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;standalone-functions&quot;&gt;Standalone functions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Standalone functions are better when tree-shaking is important or when you prefer explicit call sites.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { registerPasskey } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/browser&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;registerPasskey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ username: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;jdoe@example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The function import gives bundlers a smaller surface to analyze. It also makes each operation’s config dependency visible at the call site.&lt;/p&gt;
&lt;p&gt;Server functions follow the same shape:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { exchangeCode } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/server&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;exchangeCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, apiKey: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myApiKey&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;safe-entry-points&quot;&gt;Safe entry points&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The default package entry points are safe:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Passlock, registerPasskey } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/browser&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Passlock &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; ServerPasslock, exchangeCode } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/server&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Safe functions and methods return a result envelope for expected Passlock errors. You branch on &lt;code dir=&quot;auto&quot;&gt;result.success&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;result.failure&lt;/code&gt;, and TypeScript narrows the success and error paths.&lt;/p&gt;
&lt;p&gt;That shape is useful when errors are part of normal product flow: unsupported passkeys, duplicate credentials, invalid one-time codes, expired challenges, or failed verification.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { isPasskeyUnsupportedError, registerPasskey } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/browser&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;registerPasskey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ username: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;jdoe@example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;failure&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isPasskeyUnsupportedError&lt;/span&gt;&lt;span&gt;(result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;This device does not support passkeys&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Unexpected runtime failures can still throw. Safe entry points are about typed handling for expected Passlock outcomes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;unsafe-entry-points&quot;&gt;Unsafe entry points&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Unsafe entry points live under &lt;code dir=&quot;auto&quot;&gt;/unsafe&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Passlock, registerPasskey } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/browser/unsafe&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Passlock &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; ServerPasslock, exchangeCode } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/server/unsafe&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Unsafe functions and methods resolve with the success payload directly. Expected Passlock errors reject the promise, so you handle them with &lt;code dir=&quot;auto&quot;&gt;try&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;catch&lt;/code&gt; and narrow with the exported type guards.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;isDuplicatePasskeyError,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;registerPasskey,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/browser/unsafe&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;registerPasskey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ username: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;jdoe@example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (error) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;isDuplicatePasskeyError&lt;/span&gt;&lt;span&gt;(error)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;This passkey is already registered&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This style is useful when the rest of your application already uses thrown errors and centralized exception handling.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;choosing-a-style&quot;&gt;Choosing a style&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;There is no special capability hidden behind one style. Pick the one that best fits the code you are writing.&lt;/p&gt;
&lt;p&gt;| Approach | Best fit |
|:—|:—|
| Class safe | Shared config and explicit result handling |
| Class unsafe | Shared config and thrown-error application style |
| Function safe | Tree-shakeable imports and explicit result handling |
| Function unsafe | Tree-shakeable imports and thrown-error application style |&lt;/p&gt;
&lt;p&gt;Our default recommendation is simple: start with the safe &lt;code dir=&quot;auto&quot;&gt;Passlock&lt;/code&gt; class for application code, then move hot browser paths to standalone safe functions when bundle size matters. Use unsafe entry points when thrown errors fit your existing conventions better.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/style/&quot;&gt;Choose your code style&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apidocs.passlock.dev/browser/index.html&quot;&gt;@passlock/browser API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apidocs.passlock.dev/server/index.html&quot;&gt;@passlock/server API reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Why we launched Passlock as a cloud based toolkit</title><link>https://passlock.dev/blog/why-we-launched-in-the-cloud/</link><guid isPermaLink="true">https://passlock.dev/blog/why-we-launched-in-the-cloud/</guid><description>Why did we choose to launch Passlock as a cloud platform instead of a self-hosted solution? Because modern authentication isn’t just a feature—it’s infrastructure. In this post, we explore how a cloud-first approach enables faster iteration, stronger security, and a simpler developer experience when working with passkeys and WebAuthn.

</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When we set out to build Passlock, we had a choice:&lt;/p&gt;
&lt;p&gt;Should this be a self-hosted library developers run themselves, or a fully managed cloud platform?&lt;/p&gt;
&lt;p&gt;On the surface, a library might seem simpler. Developers get full control, no external dependency, and everything runs in their own infrastructure. But after working deeply with WebAuthn—and seeing the real-world complexity behind passkeys—we made a deliberate decision:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Passlock would be cloud-first.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here’s why.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;authentication-is-not-a-featureits-infrastructure&quot;&gt;Authentication is not a “feature”—it’s infrastructure&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Authentication sits on the critical path of your application. If it breaks, users can’t log in. If it’s insecure, everything else is at risk.&lt;/p&gt;
&lt;p&gt;Building on top of a library gives you primitives—but it also means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Designing your own credential storage&lt;/li&gt;
&lt;li&gt;Handling edge cases across browsers and devices&lt;/li&gt;
&lt;li&gt;Managing security, replay protection, and abuse prevention&lt;/li&gt;
&lt;li&gt;Maintaining and evolving the system over time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, you’re not just “adding auth”—you’re &lt;strong&gt;building and operating an authentication service&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Passlock exists to take that off your plate.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;language-and-framework-agnostic-by-design&quot;&gt;Language and framework agnostic by design&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One of our core goals was to avoid locking developers into a specific stack.&lt;/p&gt;
&lt;p&gt;While we provide a &lt;a href=&quot;https://www.npmjs.com/package/@passlock/server&quot;&gt;JavaScript server library&lt;/a&gt;, it’s intentionally thin - just a wrapper around our &lt;a href=&quot;https://passlock.dev/rest-api/&quot;&gt;REST API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That means Passlock works with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JavaScript / TypeScript (Node, Bun, Deno, edge runtimes)&lt;/li&gt;
&lt;li&gt;Python, Java, Go&lt;/li&gt;
&lt;li&gt;Or any backend capable of making HTTP requests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By running as a cloud service, we can offer a &lt;strong&gt;consistent, language-agnostic interface&lt;/strong&gt; instead of maintaining multiple deeply integrated SDKs across ecosystems.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;avoiding-deployment-complexity-for-everyone&quot;&gt;Avoiding deployment complexity (for everyone)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If Passlock were self-hosted, we wouldn’t just be shipping code—we’d be shipping infrastructure.&lt;/p&gt;
&lt;p&gt;That would mean supporting combinations like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker vs native installs&lt;/li&gt;
&lt;li&gt;Kubernetes vs simple VM deployments&lt;/li&gt;
&lt;li&gt;Serverless environments&lt;/li&gt;
&lt;li&gt;Multiple database backends (Postgres, MySQL, MongoDB, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each combination introduces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different failure modes&lt;/li&gt;
&lt;li&gt;Different performance characteristics&lt;/li&gt;
&lt;li&gt;Different operational challenges&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By running Passlock as a cloud platform, we eliminate that entire class of complexity—for both you and us.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;faster-iteration-in-a-fast-moving-space&quot;&gt;Faster iteration in a fast-moving space&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;WebAuthn and passkeys are still evolving.&lt;/p&gt;
&lt;p&gt;Browsers are changing behaviour. New capabilities are being introduced. Edge cases are constantly being discovered.&lt;/p&gt;
&lt;p&gt;At the same time, we have an aggressive roadmap for Passlock itself.&lt;/p&gt;
&lt;p&gt;If we shipped a self-hosted product, we’d need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support multiple versions “in the wild”&lt;/li&gt;
&lt;li&gt;Maintain backward compatibility across deployments&lt;/li&gt;
&lt;li&gt;Slow down changes to avoid breaking existing installations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By running in the cloud, we can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ship improvements immediately&lt;/li&gt;
&lt;li&gt;Fix issues once, for everyone&lt;/li&gt;
&lt;li&gt;Continuously refine behaviour as the ecosystem evolves&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This lets us move &lt;strong&gt;much faster than a traditional self-hosted model allows&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;seamless-data-migrations&quot;&gt;Seamless data migrations&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Authentication data isn’t static.&lt;/p&gt;
&lt;p&gt;As new features are introduced—or as the WebAuthn spec evolves—data structures may need to change.&lt;/p&gt;
&lt;p&gt;In a self-hosted world, that means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing migration scripts&lt;/li&gt;
&lt;li&gt;Coordinating deployments&lt;/li&gt;
&lt;li&gt;Risking inconsistencies across environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With Passlock in the cloud:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We handle migrations centrally&lt;/li&gt;
&lt;li&gt;Changes are applied safely and consistently&lt;/li&gt;
&lt;li&gt;Everything is transparent to you&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t need to think about schema changes—we take care of it.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;instant-security-patching&quot;&gt;Instant security patching&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Security issues don’t wait for release cycles.&lt;/p&gt;
&lt;p&gt;In a self-hosted model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A vulnerability is discovered&lt;/li&gt;
&lt;li&gt;A patch is released&lt;/li&gt;
&lt;li&gt;You need to upgrade&lt;/li&gt;
&lt;li&gt;You need to deploy&lt;/li&gt;
&lt;li&gt;You need to verify everything still works&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That gap—between patch release and deployment—is where risk lives.&lt;/p&gt;
&lt;p&gt;With Passlock:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We patch vulnerabilities immediately&lt;/li&gt;
&lt;li&gt;Fixes are applied globally&lt;/li&gt;
&lt;li&gt;No action is required from you&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your application benefits from the latest security improvements automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;reducing-the-hidden-cost-of-webauthn&quot;&gt;Reducing the hidden cost of WebAuthn&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;WebAuthn is powerful—but it’s not trivial.&lt;/p&gt;
&lt;p&gt;Behind the scenes, there are complexities around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browser inconsistencies (especially Safari)&lt;/li&gt;
&lt;li&gt;Credential lifecycle management&lt;/li&gt;
&lt;li&gt;Cross-origin and RP ID behaviour&lt;/li&gt;
&lt;li&gt;Device-specific quirks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These aren’t things you solve once. They’re things you continuously maintain.&lt;/p&gt;
&lt;p&gt;Passlock centralises that effort, so every improvement benefits every developer using the platform.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-trade-off-control-vs-velocity&quot;&gt;The trade-off: control vs velocity&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Choosing a cloud platform is ultimately a trade-off.&lt;/p&gt;
&lt;p&gt;With Passlock, you’re choosing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster development&lt;/li&gt;
&lt;li&gt;Less operational overhead&lt;/li&gt;
&lt;li&gt;Continuous improvements&lt;/li&gt;
&lt;li&gt;Centralised security and reliability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In exchange for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Relying on an external service&lt;/li&gt;
&lt;li&gt;Less direct control over the underlying system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most teams, especially those where authentication isn’t the core product, that’s the right trade.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;our-philosophy&quot;&gt;Our philosophy&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We don’t see Passlock as just an API.&lt;/p&gt;
&lt;p&gt;We see it as &lt;strong&gt;authentication infrastructure, delivered as a service&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You focus on building your product.&lt;br&gt;
We handle the complexity of passkeys, WebAuthn, and everything that comes with them.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you’re building with passkeys and want to move fast without owning the entire auth stack, Passlock is designed for you.&lt;/p&gt;</content:encoded></item><item><title>Migrating passkeys to a new domain</title><link>https://passlock.dev/blog/migrating-passkeys-domain/</link><guid isPermaLink="true">https://passlock.dev/blog/migrating-passkeys-domain/</guid><description>Changing domains does not have to mean breaking passkey sign-in. Learn how related origin requests work and how Passlock removes most of the migration plumbing.

</description><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Changing domains is usually when developers realise their user’s passkeys only work on the existing domain 💥&lt;/p&gt;
&lt;p&gt;A passkey created for &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; is not automatically available on &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;. That domain binding is part of what makes passkeys resistant to phishing, but it also means a rebrand, acquisition, or infrastructure move can break sign-in if you treat passkeys like portable usernames and passwords.&lt;/p&gt;
&lt;p&gt;Related origin requests are the standards-based answer. They let &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt; ask the browser for a passkey that was originally created for &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt;, but only if &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; explicitly trusts the new origin.&lt;/p&gt;
&lt;aside aria-label=&quot;Not export/import&quot;&gt; &lt;p aria-hidden=&quot;true&quot;&gt; Not export/import &lt;/p&gt;  &lt;div&gt;&lt;p&gt;Related origin requests do not copy passkeys between domains. They simply allow the browser to present an existing credential across a trusted domain boundary. You still need the corresponding public key and metadata on the server side.&lt;/p&gt;&lt;/div&gt; &lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;why-domain-migration-gets-awkward-quickly&quot;&gt;Why domain migration gets awkward quickly&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The browser piece is only part of the problem. A real migration usually forces you to answer several application-level questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Should you keep issuing passkeys against &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt;, or switch new registrations to &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;How will &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; publish a valid &lt;code dir=&quot;auto&quot;&gt;/.well-known/webauthn&lt;/code&gt; file over HTTPS?&lt;/li&gt;
&lt;li&gt;How will you decide whether a given login attempt should request &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;How will you track which users still have legacy passkeys?&lt;/li&gt;
&lt;li&gt;When a user finally registers a new-domain passkey, how will you retire the old one?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is where developers usually end up writing migration-specific authentication logic.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-raw-standards-require&quot;&gt;What the raw standards require&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The core WebAuthn rule is simple: the source domain must explicitly trust the calling origin.&lt;/p&gt;
&lt;p&gt;If you want &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt; to authenticate passkeys that were created for &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt;, then &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; needs to host this file:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;https://oldsite.com/.well-known/webauthn&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;origins&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;https://oldsite.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;https://newsite.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That solves only the browser permission check.&lt;/p&gt;
&lt;p&gt;You still need to choose a migration strategy. The two main options are covered in our docs guide to &lt;a href=&quot;https://passlock.dev/passkeys/related-origin-requests/&quot;&gt;Related origin requests&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep using &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; as the rpID indefinitely, even from &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Switch the tenancy to &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt; for all new registrations, while continuing to accept legacy &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; passkeys during the transition.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice the second option is usually what people mean by a domain migration.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-passlock-takes-care-of-the-complexity&quot;&gt;Where Passlock takes care of the complexity&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Passlock already documents the low-level rules in &lt;a href=&quot;https://passlock.dev/passkeys/related-origin-requests/&quot;&gt;Migrating passkeys to a new domain&lt;/a&gt;. What Passlock adds is the application plumbing around those rules.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;1-configure-the-new-steady-state-in-the-passlock-console&quot;&gt;1. Configure the new steady state in the Passlock console&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Once you are ready to start issuing passkeys for the new domain, update the tenancy so the primary rpID is &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; remains available as a related origin:&lt;/p&gt;
&lt;picture&gt; &lt;source srcset=&quot;/_astro/after.C-3I3P4l_ZPFgr1.avif&quot; type=&quot;image/avif&quot;&gt;&lt;source srcset=&quot;/_astro/after.C-3I3P4l_mQnWb.webp&quot; type=&quot;image/webp&quot;&gt;  &lt;img src=&quot;https://passlock.dev/_astro/after.C-3I3P4l_Z1msCRJ.png&quot; alt=&quot;Passlock tenancy settings showing a new primary rpID with a legacy related origin&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1756&quot; height=&quot;776&quot;&gt; &lt;/picture&gt;
&lt;picture&gt; &lt;source srcset=&quot;/_astro/after-dark.B6hCy88d_bSC0r.avif&quot; type=&quot;image/avif&quot;&gt;&lt;source srcset=&quot;/_astro/after-dark.B6hCy88d_ujowf.webp&quot; type=&quot;image/webp&quot;&gt;  &lt;img src=&quot;https://passlock.dev/_astro/after-dark.B6hCy88d_2seUV.png&quot; alt=&quot;Passlock tenancy settings showing a new primary rpID with a legacy related origin&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1756&quot; height=&quot;778&quot;&gt; &lt;/picture&gt;
&lt;p&gt;New registrations now use the new domain, while legacy passkeys can still be accepted.&lt;/p&gt;
&lt;p&gt;After that, your normal Passlock registration flow continues to work. New passkeys are created against the tenancy’s current rpID, so you do not need a separate migration-specific registration path.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;2-ask-for-the-legacy-rpid-only-when-you-need-it&quot;&gt;2. Ask for the legacy rpID only when you need it&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The awkward part of migration is authentication, not registration.&lt;/p&gt;
&lt;p&gt;A browser cannot present passkeys from &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt; in one combined request. Your application has to decide which rpID to ask for.&lt;/p&gt;
&lt;p&gt;Passlock exposes that decision as a simple override on the normal browser helper:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { authenticatePasskey } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/browser&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;authenticatePasskey&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rpId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;oldsite.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}, { tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That is the main benefit during migration. You do not need to hand-roll WebAuthn option endpoints or custom browser ceremony code for the legacy case. You keep using &lt;code dir=&quot;auto&quot;&gt;authenticatePasskey()&lt;/code&gt;, and only switch the rpID when your flow determines the user is still on the old domain.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;3-use-passlocks-credential-metadata-to-drive-the-migration&quot;&gt;3. Use Passlock’s credential metadata to drive the migration&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The hardest operational part is usually knowing which passkeys are still legacy credentials.&lt;/p&gt;
&lt;p&gt;Passlock keeps the underlying WebAuthn credential mapping, including the credential’s &lt;code dir=&quot;auto&quot;&gt;rpId&lt;/code&gt;, available through its APIs. The &lt;a href=&quot;https://passlock.dev/rest-api/get-passkey/&quot;&gt;Get passkey&lt;/a&gt; response includes &lt;code dir=&quot;auto&quot;&gt;credential.rpId&lt;/code&gt;, which lets your backend reason about whether a passkey belongs to &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { getPasskey } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/server/unsafe&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;passkey&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;getPasskey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;passkeyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myPasskeyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}, { tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, apiKey: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myApiKey&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (passkey&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;credential&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rpId&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;oldsite.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Offer the legacy login path or prompt for a replacement passkey&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That same metadata is also what Passlock uses for device-side maintenance operations such as updating or deleting passkeys. You do not need to manually preserve raw WebAuthn identifiers just to make the migration workable later.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;4-replace-and-retire-old-passkeys-with-the-same-library-surface&quot;&gt;4. Replace and retire old passkeys with the same library surface&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Once a user has authenticated with a legacy passkey, the migration path becomes straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Let them complete sign-in using &lt;code dir=&quot;auto&quot;&gt;authenticatePasskey({ rpId: &quot;oldsite.com&quot; }, { tenancyId })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Prompt them to register a fresh passkey on &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Delete the old passkey from your vault and, where supported, from the local device.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Passlock already has docs and helpers for the cleanup step, including &lt;a href=&quot;https://passlock.dev/passkeys/passkey-removal/&quot;&gt;backend and device passkey deletion&lt;/a&gt;. That means the migration does not stop at “the user managed to sign in once”; you can actually move them onto the new domain and retire the legacy credential.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-practical-migration-flow&quot;&gt;A practical migration flow&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you are migrating from &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;, the typical Passlock flow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Update your Passlock tenancy so new registrations use &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;https://oldsite.com/.well-known/webauthn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep standard registration on the new domain for all new or replacement passkeys.&lt;/li&gt;
&lt;li&gt;Use a &lt;a href=&quot;https://passlock.dev/passkeys/authentication-patterns/&quot;&gt;two-step authentication flow&lt;/a&gt; or a dedicated “Sign in with your old domain passkey” action when a user may still be carrying an &lt;code dir=&quot;auto&quot;&gt;oldsite.com&lt;/code&gt; credential.&lt;/li&gt;
&lt;li&gt;After a successful legacy login, register a replacement passkey on &lt;code dir=&quot;auto&quot;&gt;newsite.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Retire the old passkey once the new one is confirmed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That leaves each piece of complexity in the right place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The standards details live in the &lt;a href=&quot;https://passlock.dev/passkeys/related-origin-requests/&quot;&gt;Related origin requests&lt;/a&gt; guide.&lt;/li&gt;
&lt;li&gt;The application helpers live in &lt;code dir=&quot;auto&quot;&gt;@passlock/browser&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;@passlock/server&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The credential metadata needed for migration stays available through Passlock’s REST API and server helpers.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/passkeys/related-origin-requests/&quot;&gt;Related origin requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/passkeys/authentication-patterns/&quot;&gt;Passkey authentication patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/rest-api/get-passkey/&quot;&gt;Get passkey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/passkeys/passkey-removal/&quot;&gt;Deleting passkeys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Introducing mailbox challenges</title><link>https://passlock.dev/blog/mailboxchallenges/</link><guid isPermaLink="true">https://passlock.dev/blog/mailboxchallenges/</guid><description>We&apos;re excited to announce the launch of mailbox challenges, a generic email verification tool. Passlock&apos;s challenges apply strong security practices by default. They can be used for signups, one time logins, account email change verification and many other use cases.

</description><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;span&gt;New feature&lt;/span&gt; Passlock now supports &lt;strong&gt;mailbox challenges&lt;/strong&gt;, a server-side feature for building email one-time code flows without rebuilding the awkward security and lifecycle pieces yourself.&lt;/p&gt;
&lt;p&gt;This is aimed at the cases teams keep needing even when passkeys are the long-term goal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;verifying email ownership during signup&lt;/li&gt;
&lt;li&gt;passwordless login when passkeys are not available&lt;/li&gt;
&lt;li&gt;confirming a new email address before updating an account&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-mailbox-challenge-is&quot;&gt;What a mailbox challenge is&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A mailbox challenge is an email verification flow with a little more structure than “generate a six-digit code and hope for the best”.&lt;/p&gt;
&lt;p&gt;When you create a challenge, Passlock returns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;challengeId&lt;/code&gt; to identify the challenge&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;secret&lt;/code&gt; for your app to keep server-side&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;code&lt;/code&gt; for you to deliver by email&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;message.html&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;message.text&lt;/code&gt; if you want ready-made email content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The user receives the code, enters it into your app, and your backend verifies the attempt using &lt;strong&gt;all three parts&lt;/strong&gt;: &lt;code dir=&quot;auto&quot;&gt;challengeId&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;secret&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;code&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That detail matters. The emailed code alone is not enough.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-we-built-it&quot;&gt;Why we built it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Email one-time codes look simple until you try to ship them properly.&lt;/p&gt;
&lt;p&gt;You need challenge expiry, retry handling, invalidation of older outstanding codes, rate limiting, consistent verification, and a way to reduce the risk of treating an intercepted code as sufficient proof on its own.&lt;/p&gt;
&lt;p&gt;Mailbox challenges package those moving parts into a small API so you can focus on your product flow instead of rebuilding verification plumbing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;typical-flow&quot;&gt;Typical flow&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The flow is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your backend creates a mailbox challenge for a purpose like &lt;code dir=&quot;auto&quot;&gt;signup&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;login&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;email-change&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Your app stores &lt;code dir=&quot;auto&quot;&gt;challengeId&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;secret&lt;/code&gt; in a server-side session or HTTP-only cookie.&lt;/li&gt;
&lt;li&gt;Your mailer sends the code to the user, using Passlock’s rendered message or your own template.&lt;/li&gt;
&lt;li&gt;The user submits the code.&lt;/li&gt;
&lt;li&gt;Your backend verifies the challenge and checks the returned &lt;code dir=&quot;auto&quot;&gt;purpose&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;email&lt;/code&gt;, and any expected local user context before completing the action.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you need to restore an in-progress flow, you can also read a challenge without exposing the secret or code.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;using-passlockserver&quot;&gt;Using @passlock/server&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you are already using the Passlock server library, the basic shape looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;createMailboxChallenge,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;verifyMailboxChallenge,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/server/unsafe&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;created&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;createMailboxChallenge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;jdoe@example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;purpose: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;invalidateOthers: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}, { tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, apiKey: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myApiKey&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;savePendingLoginSession&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;challengeId: created&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challenge&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challengeId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;secret: created&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challenge&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;secret&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sendEmail&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;to: created&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challenge&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;html: created&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challenge&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;text: created&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challenge&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;pending&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;readPendingLoginSession&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;verified&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;verifyMailboxChallenge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;challengeId: &lt;/span&gt;&lt;span&gt;pending&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challengeId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;secret: &lt;/span&gt;&lt;span&gt;pending&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;secret&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;code: &lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}, { tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, apiKey: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myApiKey&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (verified&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challenge&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;purpose&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Unexpected challenge purpose&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;completeLoginForEmail&lt;/span&gt;&lt;span&gt;(verified&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;challenge&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The same feature is also available over raw HTTP if you are not using the JS/TS server library.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-passlock-does-not-send-the-email-for-you&quot;&gt;Why Passlock does not send the email for you&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Passlock creates the challenge and renders the message content, but the email still comes from your system.&lt;/p&gt;
&lt;p&gt;That is deliberate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the email should come from your domain&lt;/li&gt;
&lt;li&gt;you keep control over deliverability and branding&lt;/li&gt;
&lt;li&gt;you can either send the rendered HTML/text directly or generate your own template from the raw code&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;where-mailbox-challenges-fit&quot;&gt;Where mailbox challenges fit&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Mailbox challenges are not a replacement for passkeys. They cover a different part of the authentication surface.&lt;/p&gt;
&lt;p&gt;Passkeys remain the stronger default for ongoing authentication. Mailbox challenges are useful when you need to prove mailbox ownership, offer a passwordless fallback, or verify account changes that are naturally email-centric.&lt;/p&gt;
&lt;p&gt;In practice, many applications will use both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;passkeys for primary authentication&lt;/li&gt;
&lt;li&gt;mailbox challenges for onboarding, recovery, or account-change verification&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/one-time-codes/&quot;&gt;One-time codes via email&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/one-time-codes/account-signup/&quot;&gt;Signup verification flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/one-time-codes/logins/&quot;&gt;Passwordless login flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/one-time-codes/account-management/&quot;&gt;Account management and email change verification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/one-time-codes/reference/&quot;&gt;Mailbox challenge reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/rest-api/mailbox-challenges/&quot;&gt;Mailbox challenge REST API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Updating passkeys on user devices</title><link>https://passlock.dev/blog/signalcurrentuserdetails/</link><guid isPermaLink="true">https://passlock.dev/blog/signalcurrentuserdetails/</guid><description>Passkeys are comprised of server-side and client-side components. They should be kept in sync to avoid user confusion, frustration, and abandonment. Learn how to programmatically edit and delete passkeys in your users&apos; passkey managers, i.e. on their local devices.

</description><pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WebAuthn Level 3 adds &lt;code dir=&quot;auto&quot;&gt;PublicKeyCredential.signalCurrentUserDetails()&lt;/code&gt;, a small but useful API for keeping the passkey names shown on a user’s device aligned with the account details they use today.&lt;/p&gt;
&lt;p&gt;This is not about changing authentication identifiers. It is about updating the local label a user sees in their password manager after a typo is fixed or an account username or email address changes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-signalcurrentuserdetails-does&quot;&gt;What signalCurrentUserDetails() does&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;signalCurrentUserDetails()&lt;/code&gt; tells the browser or password manager to update the local metadata associated with a passkey.&lt;/p&gt;
&lt;p&gt;In practice that means you can change the name a user sees in places like Apple Passwords, Google Password Manager, or browser-managed passkey UI without re-registering the credential.&lt;/p&gt;
&lt;picture&gt; &lt;source srcset=&quot;/_astro/passkey-before-name-change.DQ2nmsBP_2JI4q.avif&quot; type=&quot;image/avif&quot;&gt;&lt;source srcset=&quot;/_astro/passkey-before-name-change.DQ2nmsBP_Z1HGKd8.webp&quot; type=&quot;image/webp&quot;&gt;  &lt;img src=&quot;https://passlock.dev/_astro/passkey-before-name-change.DQ2nmsBP_ZuwxJ.png&quot; loading=&quot;eager&quot; alt=&quot;Passkey before a device local name change&quot; decoding=&quot;async&quot; width=&quot;1597&quot; height=&quot;1158&quot;&gt; &lt;/picture&gt;
&lt;p&gt;Becomes…&lt;/p&gt;
&lt;picture&gt; &lt;source srcset=&quot;/_astro/passkey-after-name-change.BVqMpZ8J_ZTaCEU.avif&quot; type=&quot;image/avif&quot;&gt;&lt;source srcset=&quot;/_astro/passkey-after-name-change.BVqMpZ8J_1Esc9R.webp&quot; type=&quot;image/webp&quot;&gt;  &lt;img src=&quot;https://passlock.dev/_astro/passkey-after-name-change.BVqMpZ8J_1ROCsw.png&quot; alt=&quot;Passkey after a device local name change&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1585&quot; height=&quot;1160&quot;&gt; &lt;/picture&gt;
&lt;p&gt;It is important to keep the scope clear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It updates local display metadata on the device.&lt;/li&gt;
&lt;li&gt;It does not change the credential ID.&lt;/li&gt;
&lt;li&gt;It does not change the WebAuthn &lt;code dir=&quot;auto&quot;&gt;user.id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It does not change your backend account identifier.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;why-passkey-names-need-to-change&quot;&gt;Why passkey names need to change&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Passkeys often outlive the account data they were created with.&lt;/p&gt;
&lt;p&gt;Someone might register a passkey with &lt;code dir=&quot;auto&quot;&gt;jonh@example.com&lt;/code&gt;, then later fix the typo to &lt;code dir=&quot;auto&quot;&gt;john@example.com&lt;/code&gt;. Someone else might rename an account from a personal email address to a work email address. In both cases the passkey can continue to authenticate correctly, but the label shown in the password manager is now stale.&lt;/p&gt;
&lt;p&gt;That stale label creates needless confusion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the user sees the wrong email address when choosing a passkey&lt;/li&gt;
&lt;li&gt;support teams have to reason about old and new names&lt;/li&gt;
&lt;li&gt;the device appears out of sync with the account even though authentication still works&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;signalCurrentUserDetails()&lt;/code&gt; fixes that gap. It lets you align the device-local passkey name with the current account username and display name.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;using-the-raw-webauthn-api-in-browser-javascript&quot;&gt;Using the raw WebAuthn API in browser JavaScript&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The raw API is client-side only. You should first check for support, then pass the relying party ID, the credential’s WebAuthn &lt;code dir=&quot;auto&quot;&gt;userId&lt;/code&gt;, and the new names:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;credential&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rpId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Base64URL encoded credential userId (binary)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;userId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;MTVkMTFmdHM1Yzg0bDN0anpieG9w&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;updates&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;john@example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;displayName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;John Example&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; PublicKeyCredential&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;signalCurrentUserDetails&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; PublicKeyCredential&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;signalCurrentUserDetails&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;credential,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;updates,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Local passkey details updated&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;This browser does not support signalCurrentUserDetails()&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Two details matter here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;name&lt;/code&gt; is the username-like label shown for the passkey.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;userId&lt;/code&gt; is the credential’s WebAuthn user ID, not your own arbitrary application user ID.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That &lt;code dir=&quot;auto&quot;&gt;rpId&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;userId&lt;/code&gt; data usually comes from passkey metadata you already store on the backend. If you are using Passlock, this is exposed through the credential mapping described in &lt;a href=&quot;https://passlock.dev/rest-api/credential/&quot;&gt;the credential property docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Because browser support is still evolving, always feature-detect the API and fall back to telling the user how to update or recreate the passkey manually when the signal is unavailable.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;using-the-passlockbrowser-library&quot;&gt;Using the @passlock/browser library&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you are already using Passlock, the easier route is &lt;a href=&quot;https://apidocs.passlock.dev/browser/functions/unsafe_(default).updatePasskey.html&quot;&gt;updatePasskey()&lt;/a&gt; from the &lt;a href=&quot;https://apidocs.passlock.dev/browser/index.html&quot;&gt;@passlock/browser&lt;/a&gt; library.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { updatePasskey } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@passlock/browser&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;updatePasskey&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;passkeyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myPasskeyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;username: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;john@example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;displayName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;John Example&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}, { tenancyId: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;myTenancyId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Passlock handles the lookup for the underlying credential mapping, then signals the browser with the right &lt;code dir=&quot;auto&quot;&gt;rpId&lt;/code&gt; and credential &lt;code dir=&quot;auto&quot;&gt;userId&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Under the hood, the browser library maps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;username&lt;/code&gt; to WebAuthn &lt;code dir=&quot;auto&quot;&gt;name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;displayName&lt;/code&gt; to WebAuthn &lt;code dir=&quot;auto&quot;&gt;displayName&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you omit &lt;code dir=&quot;auto&quot;&gt;displayName&lt;/code&gt;, Passlock defaults it to the new &lt;code dir=&quot;auto&quot;&gt;username&lt;/code&gt;, which is often exactly what you want for email-based accounts.&lt;/p&gt;
&lt;p&gt;This helper only updates the local device copy of the passkey. If the user has actually changed their username or email address in your system, you should still update that server-side state separately. For Passlock vault data, see &lt;a href=&quot;https://passlock.dev/rest-api/update-passkey/&quot;&gt;Update passkey&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;keep-the-server-and-the-device-in-sync&quot;&gt;Keep the server and the device in sync&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The most reliable flow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Update the user’s account data in your own backend.&lt;/li&gt;
&lt;li&gt;Update any server-side passkey metadata you keep.&lt;/li&gt;
&lt;li&gt;Call &lt;code dir=&quot;auto&quot;&gt;signalCurrentUserDetails()&lt;/code&gt; or Passlock’s &lt;code dir=&quot;auto&quot;&gt;updatePasskey()&lt;/code&gt; in the browser to refresh the device-local label.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That gives you the best of both worlds: the server stays authoritative, and the user sees the right passkey name on their device.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/passkeys/credential-updates/&quot;&gt;Updating device passkeys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/rest-api/update-passkey/&quot;&gt;Update passkey REST API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://passlock.dev/rest-api/credential/&quot;&gt;The credential property&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apidocs.passlock.dev/browser/functions/unsafe_(default).updatePasskey.html&quot;&gt;@passlock/browser updatePasskey()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/webauthn-3/#sctn-signalCurrentUserDetails&quot;&gt;WebAuthn spec: signalCurrentUserDetails()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>