If you're looking to enhance your Solid.js app's authentication by adding passkeys, you're in the right place! In this tutorial, we'll guide you through the process of integrating passkey authentication into your existing Solid app.

We'll assume that you already have an authentication system in place, so we'll focus specifically on adding a passkey functionality to complement your current setup.
To bring passkeys to your app, you'll typically need two things:
So, let's get started on upgrading your app's login experience! To make it easier for you to follow along, we have created a sample app that you can use as a reference throughout the tutorial.
We'll be implementing passkeys in two phases.
This is what the general flow looks like. There will be two steps ("start" and "finalize"), which pass through the frontend, backend, and Passkey API.
1. Start Registration: Client requests server to start registration, server fetches creationOptions from Passkey API, and passes them to the client.
2. Create Credential: Client creates the passkey using creationOptions.
3. Finalize Registration: Client sends credential to server, server validates it with Passkey API, and confirms registration to client.

Let's implement it in the code:
Make sure you have the passkey SDK installed in your backend.
<pre><code class="language-sh">bun add @teamhanko/passkeys-sdk
</code></pre>Start by creating two functions, startServerPasskeyRegistration and finishServerPasskeyRegistration which will be used in the /passkeys/register route in the next step.
<pre><code class="language-js">// src/lib/passkey.ts
import { tenant } from "@teamhanko/passkeys-sdk";
import db from "../db";
const passkeyApi = tenant({
apiKey: process.env.PASSKEYS_API_KEY!,
tenantId: process.env.PASSKEYS_TENANT_ID!,
});
async function startServerPasskeyRegistration(userID: string) {
const user = db.users.find((user) => user.id === userID);
if (!user) {
throw new Error("User not found");
}
const createOptions = await passkeyApi.registration.initialize({
userId: user.id,
username: user.email || "",
});
return createOptions;
}
async function finishServerPasskeyRegistration(credential: any) {
await passkeyApi.registration.finalize(credential);
}
</code></pre>Let's breakdown the above code step by step:
Step 1: Import the necessary dependencies
tenant function from the @teamhanko/passkeys-sdk package to initialize the Passkey API.Step 2: Initialize the Passkey API
tenant function and providing the required configuration, such as the API key and tenant ID.Step 3: Implement the startServerPasskeyRegistration function
userID as a parameter and initiates the passkey registration process.db.users array based on the provided userID.passkeyApi.registration.initialize method, passing the user's ID and email as options.createOptions object received from the Passkey API.Step 4: Implement the finishServerPasskeyRegistration function
credential object as a parameter and finalizes the passkey registration process.passkeyApi.registration.finalize method, passing the credential object.Now, set up the API endpoint for passkey registration:
<pre><code class="language-js">// src/index.ts
import {
finishServerPasskeyRegistration,
startServerPasskeyRegistration,
} from "./lib/passkey";
app.post("/passkeys/register", checkAuth, async (c: Context) => {
const user = await c.get('user');
const userID = user?.id;
if (!userID) {
return c.json({ message: "Unauthorized" }, 401);
}
const { start, finish, credential } = await c.req.json();
if (start) {
const user = db.users.find((user) => user.id === c.get("user").id);
if (!user) {
return c.json({ message: "User not found" }, 404);
}
const createOptions = await startServerPasskeyRegistration(user.id);
return c.json({ createOptions });
}
if (finish) {
await finishServerPasskeyRegistration(credential);
return c.json({ message: "Registration successful" });
}
return c.json({ message: "Invalid request" }, 400);
});
</code></pre>/passkeys/register method. The checkAuth middleware is used to ensure the user is authenticated.c.get('user').id. If the user ID is not available, we return an "Unauthorized" error response.start, finish, and credential properties from the request body using c.req.json().start property is present, find the user in the db.users array based on the authenticated user's ID. startServerPasskeyRegistration function with the user's ID to initiate the passkey registration process and obtain the createOptions.createOptions in the response.finish property is present, we call the finishServerPasskeyRegistration function with the provided credential to complete the passkey registration.This API endpoint allows client to initiate and complete the passkey registration.
Install the @github/webauthn-json library in your Solid.js frontend.
<pre><code class="language-sh">bun add @github/webauthn-json
</code></pre>Next, create a registerPasskey function which will be called when the user clicks a button to register a new passkey. It handles the client-side process of registering a passkey.
<pre><code class="language-js"> const registerPasskey = async () => {
setIsLoading(true);
try {
const createOptionsResponse = await fetch("http://localhost:3000/passkeys/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: 'include',
body: JSON.stringify({ start: true, finish: false, credential: null }),
});
const { createOptions } = await createOptionsResponse.json();
console.log("createOptions", createOptions);
const credential = await create(createOptions as CredentialCreationOptionsJSON);
console.log(credential);
const response = await fetch("http://localhost:3000/passkeys/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ start: false, finish: true, credential }),
});
console.log(response);
if (response.ok) {
console.log("Registration successful");
} else {
const errorData = await response.json();
console.error(`Registration failed: ${errorData.message}`);
}
} catch (error) {
console.error('Registration error:', error);
} finally {
setIsLoading(false);
}
};
</code></pre>The function starts by sending a POST request to the server at the endpoint /passkeys/register to initiate the passkey registration process. The request includes a JSON payload with the following properties:
start: true: Indicates that the registration process is starting.finish: false: Indicates that the registration process is not yet finished.credential: null: Initially, no credential is provided.The function then awaits the response from the server and extracts the createOptions from the response JSON.
Next, the function calls the create function from @github/webauthn-json library, passing the createOptions as an argument.
After obtaining the credential, the function sends another POST request to the same endpoint, but this time with a different payload:
start: false: Indicates that the registration process is no longer starting.finish: true: Indicates that the registration process is finished.credential: The newly created passkey credential.Finally, if the response is successful, a passkey is registered for the user.
Now, to make it work we're missing one crucial step: getting the Tenant ID and API key secret from Hanko Cloud. For that, navigate over to Hanko, create an account, and set up your organization. Navigate to 'Create new project', select 'Passkey Infrastructure', and provide your project name and URL.
Note: It's recommended to create separate projects for your development and production environments. This way, you can use your frontend localhost URL for the development project and your production URL for the live project, ensuring a smooth transition between environments without the need to modify the 'App URL' later.
Obtain your Tenant ID and create an API key, then add the Tenant ID and API key secret to your backend's '.env' file.
Now that you have your secrets added to your backend's .env file, you should be all set to register a passkey. Go ahead and give it a try!
Implementing passkey login will be pretty similar to passkey registration. Create two more functions startServerPasskeyLogin and finishServerPasskeyLogin to start and finalize the login process.
<pre><code class="language-js">async function startServerPasskeyLogin() {
const options = await passkeyApi.login.initialize();
return options;
}
async function finishServerPasskeyLogin(options: any) {
const response = await passkeyApi.login.finalize(options);
return response;
}
</code></pre>These will be used in the /passkeys/login route below.
<pre><code class="language-js">app.post("/passkeys/login", async (c: Context) => {
const { start, finish, options } = await c.req.json();
try {
if (start) {
const loginOptions = await startServerPasskeyLogin();
return c.json({ loginOptions });
}
if (finish) {
const jwtToken = await finishServerPasskeyLogin(options);
const userID = await getUserID(jwtToken?.token ?? "");
const user = db.users.find((user) => user.id === userID);
if (!user) {
return c.json({ message: "Invalid user" }, 401);
}
const sessionId = uuidv4();
setUser(sessionId, user);
console.log(sessionId);
setCookie(c, "sessionId", sessionId, { httpOnly: true });
return c.json({ message: "Passkey Login successful" });
}
} catch (error) {
return c.json(
{ message: "An error occurred during the passkey login process." },
500
);
}
});
</code></pre>/passkeys/register, /passkeys/login POST route is defined.start, finish, and options properties from the request body using c.req.json().start property is present, we call the startServerPasskeyLogin function to initiate the passkey login process and obtain the loginOptions.loginOptions in the response.finish property is present, we call the finishServerPasskeyLogin function with the provided options to complete the passkey login and obtain a JWT token which the Hanko Passkey API returns.<pre><code class="language-js">// lib/get-user-id.ts
import * as jose from "jose";
const getUserID = async (token: string): Promise<string | undefined> => {
try {
const payload = jose.decodeJwt(token);
const userID = payload.sub;
return userID;
} catch (error) {
console.error("Error decoding JWT:", error);
return undefined;
}
};
export default getUserID;
</code></pre>Now we create a signInWithPasskey function. This function will interact with the backend APIs to login the user with their registered passkeys.
<pre><code class="language-js">import { get } from "@github/webauthn-json";
const signInWithPasskey = async () => {
setIsLoading(true);
try {
const createOptionsResponse = await fetch("http://localhost:3000/passkeys/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: 'include',
body: JSON.stringify({ start: true, finish: false, credential: null }),
});
const { loginOptions } = await createOptionsResponse.json();
const options = await get(loginOptions);
const response = await fetch("http://localhost:3000/passkeys/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: 'include',
body: JSON.stringify({ start: false, finish: true, options }),
});
if (response.ok) {
console.log("User logged in with passkey");
navigate("/dashboard");
} else {
const errorData = await response.json();
setError(errorData.message);
}
} catch (error) {
console.error("Error during passkey login:", error);
setError(error.message);
} finally {
setIsLoading(false);
}
};
</code></pre>Thats it! You now have a fully functional passkey login in your Solid.js app, significantly enhancing the user experience and making the authentication process more seamless for your users 🚀
You can always reach out to us on Discord if you run into any issues.

Magic links and email passcodes both promise passwordless login with low friction. But once you look at cross-device sign-in, browser behavior, email

Why we’re moving Hanko from a Kubernetes-native single-tenant setup to a multi-tenant architecture and what “cloud native” got wrong for a small team.
"Zombie passkeys" – passkeys that exist in a limbo state between client devices and authentication servers.