Add passkey authentication for your Supabase users

Add passkey authentication for your Supabase users

In this blog post, we’ll be walking through the process of adding passkey authentication to a Node.js application which already has an existing user base in Supabase. Firstly, we'll focus on effortlessly integrating the Corbado web component which handles passkey authentication. Secondly, we'll adapt our backend to integrate the existing Supabase users.

In this tutorial, we will create a simple Node.js app based on Supabase with Corbado as an authentication provider. We will pay special attention on how to integrate password-based users of an existing Supabase architecture.


We use a simple Node.js app in our backend and plain HTML for the frontend. The app has a login screen as well as a profile screen where information of the current user is displayed. Also, we will use the open-source Firebase alternative Supabase to store our users as well as their data.

With Supabase, developers can build scalable and secure web and mobile applications, leveraging its robust features like data storage, real-time updates, and user authentication.

The flow of information looks like this: the Corbado web component which is integrated into the login page handles all means of authentication and talks with the Node.js backend to make sure existing Supabase users can still login with their password as fallback and are slowly transitioned to passkeys, while new users are offered passkeys during sign-up.

The structure of this tutorial is as follows:

1. Set up Supabase backend

2. Configure Corbado project

3. Configure environment variables

4. Integrate the Corbado web component

      4.1. Create HTML frontend delivered by Node.js

      4.2. Set up profile page

      4.3 Add Corbado session management

5. Connect app to Supabase

6. Integrate existing users

      6.1. Add webhooks

      6.2. Connect local instance to the internet

7. Run the application

8. Next steps

9. Troubleshooting

10. Conclusion

The final code can be found on Github. If you want to run it straight away, make sure you have gone through steps 1-3 as you need to set up a Supabase project (step 1) as well as a Corbado project (step 2) and provide the environment variables (step 3) to the application. Afterwards, start the project by running

docker compose up

You can now visit http://localhost:19915 to test the app yourself:

Now back to the implementation details - the code of our project is structured as follows:

1. Set up Supabase backend

For this step, we head over to Supabase and create an account. Afterwards, we create a new project. Select name, password and region according to your preferences.

Then, we click on “Create new project” which makes Supabase instantiate our project. Beware this can take some time (~5-10 seconds during our test).

As we use Supabase’s user infrastructure for storing users, we do NOT need to create a user table inside Supabase.

Under Authentication > Users, we’ll now add some password-based users, by clicking on “Add user”.

Come up with an email and password and click on “create user”. Here, we used "" as email and “maxPW” as password. Make sure to auto confirm the user so it is a full-fledged account. Only then are we able to login as this user using the Supabase JavaScript client. We will also use the JavaScript client to access information stored in Supabase’s tables.

To later integrate these existing Supabase users into our app, we need to be able to tell if a user already exists in Supabase based on the provided email address. The Supabase JavaScript client does not provide a function to get a user by his email, so we must resort to something else. We’ll use the Supabase rpc functions, which are custom functions that can be defined inside the Supabase web interface and subsequently called via the Supabase JavaScript client.

Inside the Supabase web interface, we use the SQL Editor to create a new function “get_user_id_by_email” by executing the following PostgreSQL query:

This function returns the ID of a user for a given email (assuming the user exists in Supabase).

Now, we are done with the Supabase part of our app. The necessary credentials for communication with the Supabase JavaScript client can be obtained under "Settings > API". We will put the Project URL, the service_role API key and the JWT Secret into our .env file in step 3. The service_role key authorizes us as an administrator of the corresponding project, thus enabling additional API calls.


2. Configure Corbado project

Before building our frontend, we need to create a Corbado project. We start by heading over to the Corbado developer panel and create an account. After successful sign-up, we see this screen:

We click on “Integration guide” to do everything step by step:

We want to integrate via “Web component”, so that’s what we select.

We have a system with existing users, so we click “Yes”. Afterwards, we find ourselves at the overview of the developer panel (you need to confirm your account via email if it’s your first time).

We’ll head over to “Getting started” >“Integration guide”. This guides us through all the steps necessary for the integration to work.

Remember to select “Node.js” as progamming language in the top right corner.

In step 1 of the integration guide, we configure our authorized origin. The authorized origin is the browser URL of the site where the web component is integrated (with protocol and port but without path). We need it for CORS (cross-origin request sharing). In our case, we set it to http://localhost:19915. Next, we create an API secret. We need the project ID and the generated API secret later in order to communicate with Corbado’s Backend API.

In the second, optional step, we configure the webhook. Webhooks are needed, so Corbado can communicate with our backend / Supabase, e.g. for checking if a username and password of an existing user match. More details on that later.

We will later set up our webhook at http://localhost:19915/corbado-webhook with “webhookUsername” and “webhookPassword” as credentials, so we can already enter that here.

In step 3, we add our Application URL, Redirect URL and Relying Party ID. The Application URL is the URL in the frontend where the web component runs. For example, it’s used to forward users to the web component again after they clicked on an email magic link.

The Redirect URL is the URL where the user is directed to once the authentication has succeeded. In our case, this will be the /profile page, so we can enter http://localhost:19915/profile here already.

The Relying Party ID is the domain where we bind our passkeys to. The domain in the browser where a passkey is bount to must be a matching domain to the Relying Party ID. As we test locally, we set the Relying Party ID to “localhost”.

Just like that, the project in Corbado is set up!

3. Configure environment variables

We use a simple Node.js express app to deliver our plain HTML frontend. For this to work, the following Node.js environment variables should be configured in the respective .env file.

Supabase variables can be taken from step 1. PROJECT_ID, API_SECRET as well as CLI_SECRET should be taken from step 2.

4. Integrate the Corbado web component

4.1. Create HTML frontend delivered by Node.js

Inside our Node.js app, we have two screens:

  • Login screen
  • User profile screen showing information when a user is logged in

Our login screen contains only the Corbado web component which will handle the authentication. For more details on its usage, visit the web component docs.

4.2. Setup profile page

Once a user has authenticated, Corbado will create a session and redirect the user to the Redirect URL we defined beforehand (our “http://localhost:19915/profile” page).

We created a profile.ejs template that just displays some basic info about the user as well as a logout button:

4.3. Add Corbado session management in our backend

Here we need the Corbado SDK, which we can install by executing

npm install @corbado/node-sdk

Before delivering the profile page, the backend uses the Corbado SDK to retrieve the user of the currently active session.

Then, we add the user to our Supabase database if it doesn’t exist there yet. As an intermediate between our profile-page endpoint and Supabase, we created the UserService which will be explained in a minute.

Once we have got our Supabase user, we render the page displaying the Supabase user ID, email and name.

5. Connect app to Supabase

As mentioned before, we created the UserService to handle all actions concerning the Supabase JavaScript client. But first, we need to install the Supabase JavaScript client:

npm install@supabase/supabase-js

We initialize the client with the Supabase role key as parameter because we will use the auth.admin calls to manage our users and the role key authorizes us as the admin.

Remember to never expose your Supabase role key!  

The UserService contains methods which handle the following processes:

  1. create a new user
  2. verify the passwords of an existing user
  3. retrieve a user, given the user ID
  4. retrieve a user ID, given an email address

Apart from “get_user_id_by_email”, we only use predefined methods of the Supabase JavaScript client which uses internally the Supabase user infrastructure. All users are hereby stored in the auth.users table which can be viewed in the Supabase dashboard.

6. Integrate existing users

6.1. Add webhooks

In step 1, we added password-based users to our Supabase userbase. To let these existing users still login with their passwords, we set up webhooks.

Beforehand, we set up in the Corbado developer panel that the webhook we provide would be reachable via http://localhost:19915/corbado-webhook, so we create a controller for the route which handles Corbado’s webhooks.

There, we initialize the Corbado Node.js SDK using the project ID and API Secret from step 2. The webhook method below can be taken as a communication template for the Corbado webhooks.

The only methods you would have to change here are "getUserStatus" and "verifyPassword". Inside these two methods, we again use our UserService to check if a certain user exists and if a password matches for a certain user.

6.2. Connect your local instance to the internet

Corbado will attempt to call our webhooks, but currently they are hosted locally on http://localhost:19915/corbado-webhook, so we need to make them publicly available. We do so by using the Corbado CLI. It creates a tunnel between Corbado and our local instance, so Corbado can send webhooks to our local instance. Install it as described in our Corbado CLI docs. Then execute

corbado login

This will prompt you for your project ID from step 2 as well as your CLI secret which you can find in our app.

Afterwards execute

corbado subscribe http://localhost:19915

This will start the tunnel on port 19915.

7. Run the application


npm start

you should be able to run the application now. When visiting http://localhost:19915 you should see the Corbado web component:

If we login as the password-based user we created in step 1 using our password, we are offered to create a passkey after successful password authentication:

8. Next steps

We can now create tables in Supabase which have a foreign key linking to the ID of the auth.users table. Remember to configure Row Level Security (RLS) before querying.

To enforce RLS, we can then act on behalf of a specific user when using the Supabase JavaScript client. Therefore, the RLS policies will only let us perform actions that the respective user should be allowed to do. Use the following snippet to create a Supabase JavaScript client which identifies itself as a specific Supabase user whose ID is stored in userID.

This should only be done in your backend. Remember to never publicly expose your Supabase JWT secret!

9. Troubleshooting

You must remember to set up Row Level Security for tables you create in Supabase, otherwise you cannot perform any actions.

If the session initialization does not work, please make sure you have Corbado session management enabled in the developer panel under Settings > Sessions.

10. Conclusion

This tutorial showed how easy it is to add passkey authentication to a Node.js application which uses Supabase as a database provider and already has an existing user base (with passwords).

Authentication as well as session management is handled by Corbado all while we manage the users ourselves and can save user-data tied to our Supabase users instead of using a user identifier from Corbado. This means we are still independent and in control of our users and their data. If you want to read more about how you can leverage the session management to retrieve backend data, please see our Corbado docs.

Due to clear documentation and a well-structured client, Supabase is very convenient and easy to use. Although one must watch out which API keys and JWT secrets to use where and which of them to keep secret.

Integrating other authentication providers is not as easy as it could be as Supabase is optimized towards their own password-based authentication and session management solution. In general, for some features like passkeys you need external solutions because Supabase does not offer an implementation itself (yet).

Supabase also has a limited predefined interface which is why a custom rpc function was needed for mapping emails to user IDs.

Although one can create a user via the admin auth API without requiring a password, the entry in the auth.users table in Supabase for that new user will contain a hash in the password column which leads to the assumption that a random password might be generated nevertheless when creating a user without specifying a password.

Enjoyed this read?

Stay up to date with the latest news, strategies and insights about passkeys sent straight to your inbox!