8 Min.

Adding passkeys to a Svelte.js app

If you're looking to add a passkey login to your existing Svelte.js application, you're in the right place! Passkey login is a secure and user-friendly authentication method that eliminates the need for traditional passwords. By implementing passkeys, you can provide your users with a seamless and secure login experience.

In this tutorial, we'll walk you through the step-by-step process of adding passkey auth to your Svelte.js app using the Hanko Passkey API. We'll assume that you already have authentication set up, so we won't focus on that here. Instead, we'll dive straight into adding passkeys to your existing auth system.

To bring passkeys to your app, you'll typically need two things:

  1. A backend to handle the authentication flow and store your users' passkeys. In this tutorial, we'll be using Express.js for the backend. However, the process will be similar for any other Node.js-based framework you might be using.
  2. A few functions in your Svelte frontend to display and handle the "Sign in with a passkey" and "Register a passkey" dialogs. These functions will interact with your backend to facilitate the passkey authentication process.

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 passkey functionality in two phases.

  • Passkey Registration: In this phase, we'll set up the necessary things to allow users to register and store their passkey.
  • Sign In with Passkey: Once users have registered their passkeys, we'll implement the functionality to enable them to log in using their registered passkeys.

Passkey Registration

Express Backend:

Make sure you have the passkey SDK installed in your backend.

npm i @teamhanko/passkeys-sdk

The startServerPasskeyRegistration function is an asynchronous function that takes a userID parameter. It performs the following steps:

  1. Retrieves the user object from the db.users array based on the provided userID.
  2. Calls the passkeyApi.registration.initialize method with the user's ID and email (or an empty string if email is not available) to initialize the passkey registration process.
  3. Returns the createOptions object received from the SDK, which contains the necessary data to proceed with the registration on the client-side.

The finishServerPasskeyRegistration function is another asynchronous function that takes a credential parameter. It simply calls the passkeyApi.registration.finalize method with the provided credential to complete the passkey registration process on the server-side.

These functions act as service-level functions that encapsulate the logic for interacting with the Hanko Passkey SDK. They can be called from controller functions, which in turn are invoked by the corresponding routes in your application.

// services/passkey.js

import { tenant } from "@teamhanko/passkeys-sdk";
import dotenv from "dotenv";
import db from "../db.js";


const passkeyApi = tenant({
  apiKey: process.env.PASSKEYS_API_KEY,
  tenantId: process.env.PASSKEYS_TENANT_ID,

async function startServerPasskeyRegistration(userID) {
  const user = db.users.find((user) => === userID);

  const createOptions = await passkeyApi.registration.initialize({
    username: || "",

  return createOptions;

async function finishServerPasskeyRegistration(credential) {
  await passkeyApi.registration.finalize(credential);

The handlePasskeyRegister() controller function first extracts the user object from the request (req) and retrieves the userID from it.

Next, it extracts the start, finish, and credential properties from the request body. These properties are used to determine the specific action to be performed.

If the start property is present, the controller function calls thestartServerPasskeyRegistration service function, passing the userID as an argument. This function initializes the passkey registration process and returns the createOptions object. The controller then sends the createOptions back to the client as a JSON response.

If the finish property is present, the controller function calls the finishServerPasskeyRegistration service function, passing the credential obtained from the request body. This function finalizes the passkey registration process on the server-side. Once the registration is complete, the controller sends a JSON response with a success message.

// controllers/passkey.js

import {
} from "../service/passkey.js";
import db from "../db.js";

async function handlePasskeyRegister(req, res) {
  const { user } = req;
  const userID =;

  if (!userID) {
    return res.status(401).json({ message: "Unauthorized" });
  console.log("userId", userID);

  const { start, finish, credential } = req.body;

  try {
    if (start) {
      const createOptions = await startServerPasskeyRegistration(userID);
      console.log("registration start");
      return res.json({ createOptions });
    if (finish) {
      await finishServerPasskeyRegistration(credential);
      return res.json({ message: "Registered Passkey" });
  } catch (error) {
    return res.status(500).json(error);

Now, we setup a /passkeys/register POST route. When a POST request is made to this, the handlePasskeyRegister controller function is invoked to handle the passkey registration process.

// routes/passkey.js

import express from "express";
const router = express.Router();
import { handlePasskeyRegister } from "../controllers/passkey.js";
import { checkAuth } from "../middleware/auth.js";"/passkeys/register", checkAuth, handlePasskeyRegister);

export default router;

Svelte Frontend:

Make sure to install the @github/webauthn-json library in your frontend.

npm install @github/webauthn-json

Next, create a registerPasskey function that will be called when the user clicks a button to register a new passkey. It handles the client-side process of registering a passkey.

The function starts by sending a POST request to the server at the endpoint /api/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!

Passkey Login


To enable passkey login, we'll create one more service function to initiate and finish the login process.

// services/passkey.js

async function startServerPasskeyLogin() {
  const options = await passkeyApi.login.initialize();
  return options;

async function finishServerPasskeyLogin(options) {
  const response = await passkeyApi.login.finalize(options);
  return response;

Now, similar to handlePasskeyRegister create a handlePasskeyLogin controller.

This function handles process of logging in with a passkey. It receives a request with start, finish, and options properties. If start is true, it initiates the login process by calling startServerPasskeyLogin() and returns the login options. If finish is true, it completes the login process by calling finishServerPasskeyLogin() with the provided options. It then retrieves the user ID from the JWT token, finds the corresponding user in the database, creates a session ID, sets the user for the session, and logs them in.

// controllers/passkey.js

import {
} from "../service/passkey.js";
import { getUserID } from "../service/get-user-id.js";
import { v4 as uuidv4 } from "uuid";
import { setUser } from "../service/auth.js";
import db from "../db.js";

async function handlePasskeyLogin(req, res) {
  const { start, finish, options } = req.body;

  try {
    if (start) {
      const loginOptions = await startServerPasskeyLogin();
      return res.json({ loginOptions });
    if (finish) {
      const jwtToken = await finishServerPasskeyLogin(options);
      const userID = await getUserID(jwtToken?.token ?? "");
      console.log("userID from hanko", userID);
      const user = db.users.find((user) => === userID);
      if (!user) {
        return res.status(401).json({ message: "Invalid user" });
      console.log("user", user);
      const sessionId = uuidv4();
      setUser(sessionId, user);
      res.cookie("sessionId", sessionId);
      return res.json({ message: " Passkey Login successful" });
  } catch (error) {
    return res
      .json({ message: "An error occurred during the passkey login process." });

In the /passkeys/login POST route, we invoke the above controller.

// routes/passkey.js"/passkeys/login", handlePasskeyLogin);


Now we create a signInWithPasskey function. This function will interact with the backend APIs to initiate and finalize the passkey login process.

    async function signInWithPasskey() {
      try {
        const createOptionsResponse = await fetch("http://localhost:5001/api/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:5001/api/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'); // Assuming you have a route named '/dashboard'
        } else {
          throw new Error("Login with passkey failed");
      } catch (error) {
        console.error("Login with passkey error:", error);

Congratulations! You now have a fully functional passkey login in your Svelte.js app, significantly enhancing the user experience and making the authentication process more seamless for your users 🚀

If you encountered any issues or have further questions, feel free to reach out to us on Discord. We're here to help you succeed in implementing passkey authentication in your app.

Back to overview

More blog posts

Don't miss out on latest blog posts, new releases and features of Hanko's products, and more.

Your submission has been received!
Something went wrong.