---
url: 'https://www.corbado.com/blog/supabase-passkeys'
title: 'Supabase Passkeys: Add Passkeys for Your Supabase Users'
description: 'Supabase + Passkeys = Supapasskeys: this tutorial explains how to integrate passkeys with Supabase for secure and simple auth solutions.'
lang: 'en'
author: 'Nicolai'
date: '2023-07-13T00:00:00.000Z'
lastModified: '2026-03-25T10:00:20.300Z'
keywords: 'Supabase'
category: 'Passkeys Implementation'
---

# Supabase Passkeys: Add Passkeys for Your Supabase Users

> This tutorial might be outdated, as we're currently reworking our Supabase integration
> approach. Please [reach out](https://www.corbado.com/contact) if you want to get more insights.

## Key Facts

- **Corbado web component** handles all passkey authentication for Supabase apps, with
  users stored in Supabase's native `auth.users` table. No separate user table is needed.
- Existing **password-based Supabase users** are supported via webhooks: they authenticate
  with their password first, then are prompted to create a passkey for future logins.
- A custom **PostgreSQL RPC function** (`get_user_id_by_email`) is required because the
  Supabase JavaScript client has no built-in method to retrieve a user by email address.
- **Supabase does not natively support passkeys** and is optimized for its own
  password-based auth, requiring an external provider like Corbado for passkey
  implementation.
- **Row Level Security (RLS)** must be configured on all Supabase tables before querying.
  Omitting RLS will prevent any data actions from succeeding.

## 1. Introduction

In this tutorial, we will create a simple [Node.js](https://www.corbado.com/blog/nodejs-passkeys) app based on
Supabase with Corbado as an authentication provider to provide passkeys authentication
("Supapasskeys"). We will pay special attention on how to integrate password- based users
of an existing Supabase architecture.

## 2. Overview

We use a simple [Node.js](https://www.corbado.com/blog/nodejs-passkeys) 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](https://www.corbado.com/blog/nodejs-passkeys) 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.

![Supabase Passkeys: Flow of Information with Corbado and Supabase](https://www.corbado.com/website-assets/64afad18e82e050ac683c076_structure_d5a16769e6.png)

The final code can be found on
[GitHub](https://github.com/corbado/example-webcomponent-supabase). 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

```bash
docker compose up
```

You can now visit [http://localhost:19915](http://localhost:19915) to test the app
yourself:

![Supabase Passkeys: Corbado Login Page](https://www.corbado.com/website-assets/64b05276c33982116af5f01c_webcomponent_b3ae0de667.png)

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

```
├── app.js
├── .env
├── src
|   ├── controllers
|   |   ├── authController.js           # renders views and uses Corbado SDK for sessions
|   |   └── corbadoWebhookController.js # Takes all requests belonging to the Corbado webhook logic
|   ├── routes
|   |   ├── authRoutes.js               # All routes belonging to certain views
|   |   └── corbadoWebhookRoutes.js     # All routes belonging to the Corbado webhook
|   ├── services
|   |   └── userService.js              # Communicates with Supabase
|   ├── views/pages
|   |   ├── login.ejs                   # Login page with the webcomponent
|   |   └── profile.ejs                 # Profile page showing user info
```

## 3. Set Up Supabase Backend

For this step, we head over to Supabase and
[create an account](https://supabase.com/dashboard/sign-in). Afterwards, we create a new
project. Select name, password and region according to your preferences.

![Supabase Passkeys: Create new Project in Supabase](https://www.corbado.com/website-assets/64f0702350590940aeb0cb19_create_project_3efd3c3207.png)

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 Supabases user infrastructure for storing users, we do NOT need to create a user
table inside Supabase.

Under Authentication &gt; Users, well now add some password-based users, by clicking on
Add user.

![Supabase Passkeys: Create](https://www.corbado.com/website-assets/64f0704a9d88accd763ded90_users_e76f960d75.png)

Come up with an email and password and click on create user. Here, we used
"[max@company.com](mailto:max@company.com)" 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 Supabases tables.

![Supabase Passkeys: Create ](https://www.corbado.com/website-assets/64f07031f50c9e7144a09ce2_create_user_7f0f3ffb0f.png)

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. Well 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](https://www.corbado.com/blog/passkey-webauthn-database-guide) query:

```
CREATE OR REPLACE FUNCTION get_user_id_by_email(email TEXT)
RETURNS TABLE (id uuid)
SECURITY definer
AS $$
BEGIN
  RETURN QUERY SELECT au.id FROM auth.users au WHERE au.email = $1;
END;
$$ LANGUAGE plpgsql;
```

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

![Supabase Passkeys: SQL Function to return ID of a User for given email](https://www.corbado.com/website-assets/64afb8b7a6aecd6aceee41d1_supabase_create_function_b802a79f3c.png)

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 &gt;
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.

![Supabase Passkeys: Enable additional API calls in Supabase](https://www.corbado.com/website-assets/64f0706c31cd7079912ba909_supabase_credentials_1fe999d1d4.png)

## 4. Configure Corbado project

Before building our frontend, we need to create a Corbado project. We start by heading
over to the [Corbado developer panel](https://app.corbado.com/signin#register) and create
an account. After successful sign-up, we see this screen:

![Supabase Passkeys: Corbado Developer Panel create Project](https://www.corbado.com/website-assets/64f0707be7effe6dcbe5301e_intro_e87e26c856.png)

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

![Supabase Passkeys: Corbado Developer Panel Integration Guide](https://www.corbado.com/website-assets/64f07088472b994404e36bd7_intro_2_2daff178b8.png)

We want to integrate via Web component, so thats what we select.

![Supabase Passkeys: Corbado Developer Panel Web Componente](https://www.corbado.com/website-assets/64f07091e249f3850d00e9be_intro_3_598cec0e96.png)

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 its
your first time).

Well head over to Getting started >Integration guide. This guides us through all the steps
necessary for the integration to work.

![Supabase Passkeys: Corbado Developer Panel create Project](https://www.corbado.com/website-assets/64f0709e765d8fd56584ea05_integration_guide_40ff7e8757.png)

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](https://www.corbado.com/blog/iframe-passkeys-webauthn) request sharing). In our case, we set it
to [http://localhost:19915](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
Corbados Backend API.

![Supabase Passkeys: Corbado Developer Panel Configure Authentication](https://www.corbado.com/website-assets/64f070a910e8762ce7984969_integration_guide_step1_ef07391579.png)

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](http://localhost:19915/corbado-webhook) with
webhookUsername and webhookPassword as credentials, so we can already enter that here.

![Supabase Passkeys: Corbado Developer Panel create Webhooks](https://www.corbado.com/website-assets/64f070b7b1b4d53a95eef503_setup_webhooks_fde17b3263.png)

In step 3, we add our Application URL, Redirect URL and
[Relying Party](https://www.corbado.com/glossary/relying-party) ID. The Application URL is the URL in the
frontend where the web component runs. For example, its 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 ](http://localhost:19915/profile)here already.

The [Relying Party](https://www.corbado.com/glossary/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](https://www.corbado.com/glossary/relying-party) ID. As we test locally, we set the Relying Party
ID to localhost.

![Supabase Passkeys: Define URLs and Relying Party ID](https://www.corbado.com/website-assets/64f070c5302364f02ea0b509_url_settings_7fc182c0a3.png)

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

## 5. 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](#setup-supabase-backend). PROJECT_ID,
API_SECRET as well as CLI_SECRET should be taken from
[step 2](#configure-corbado-project).

```dotenv
// .env

PROJECT_ID=""
API_SECRET=""
CLI_SECRET=""

WEBHOOK_USERNAME="webhookUsername"
WEBHOOK_PASSWORD="webhookPassword"

SUPABASE_URL=""
SUPABASE_API_KEY_SERVICE_ROLE=""
SUPABASE_JWT_SECRET=""
```

## 6. Integrate the Corbado Web Component

### 6.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.

```html
// src/views/pages/login.ejs

<!DOCTYPE html>
<html>
    <body>
        <!-- Your website content -->

        <style>
            corbado-auth {
                --primary-color: #1953ff;
                --primary-color-rgb: 25, 83, 255;
                --heading-color: #090f1f;
                --text-color: #535e80;
                --light-color: #8f9bbf;
                --error-color: #ff4c51;
                --primary-font: "Space Grotesk", sans-serif;
                --secondary-font: "Inter", sans-serif;
                --border-color: rgba(143, 155, 191, 0.5);
            }
        </style>

        <script
            defer
            src="https://<%= process.env.PROJECT_ID %>.frontendapi.corbado.io/auth.js"
        ></script>
        <corbado-auth project-id="<%= process.env.PROJECT_ID %>" conditional="yes">
            <input
                name="username"
                id="corbado-username"
                value=""
                data-input="username"
                required
                autocomplete="webauthn"
            />
        </corbado-auth>
    </body>
</html>
```

### 6.2 Set Up 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](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:

```
// src/views/pages/profile.ejs

<!DOCTYPE html>
<html>

<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
    <style>
        body {
            padding: 2em;
        }
        button {
            border-radius: 5px;
            padding: 3px;
        }
    </style>
</head>

<body>
    <script src="https://<%= process.env.PROJECT_ID %>.frontendapi.corbado.io/auth.js"></script>

    <corbado-auth-provider project-id="<%= process.env.PROJECT_ID %>">
        <div slot="authed">
            <div class="main-container">
                <h1>Welcome!</h1>

                <p>Email: <%= username %><br>
                        Name: <%= userFullName %><br>
                            SupabaseID: <%= supabaseID %><br>
                </p>

            </div>
            <corbado-logout-handler project-id="<%= process.env.PROJECT_ID %>" redirect-url="/logout">
                <button>Logout</button>
            </corbado-logout-handler>
        </div>
    </corbado-auth-provider>

</body>

</html>
```

### 6.3 Add Corbado Session Management

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

```bash
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 doesnt 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](https://www.corbado.com/blog/webauthn-user-id-userhandle), email and name.

```javascript
// src/controllers/authController.js

export const profile = async (req, res) => {
    try {
        const { email, name } = await corbado.session.getCurrentUser(req);
        const user = await UserService.findByEmail(email);
        const userId = user?.id;
        if (!userId) {
            // Create new user
            UserService.create(email, name).then((u) => {
                if (u == null) {
                    res.redirect("/logout");
                } else {
                    const user = u.user;
                    res.render("pages/profile", {
                        username: user.email,
                        userFullName: user.user_metadata.name,
                        supabaseID: user.id,
                    });
                }
            });
        } else {
            // User already exists
            res.render("pages/profile", {
                username: user.email,
                userFullName: user.user_metadata.name,
                supabaseID: user.id,
            });
        }
    } catch (err) {
        console.error(err);
        res.status(500).send("Server Error");
    }
};
```

## 7. Connect Passkeys 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:

```bash
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](https://www.corbado.com/blog/webauthn-user-id-userhandle)
4. retrieve a [user ID](https://www.corbado.com/blog/webauthn-user-id-userhandle), 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.

```javascript
// src/services/userService.js

import { createClient } from "@supabase/supabase-js";

import { config as dotenvConfig } from "dotenv";
dotenvConfig();

const supabaseUrl = process.env.SUPABASE_URL;
const supabaseRoleKey = process.env.SUPABASE_API_KEY_SERVICE_ROLE;

const supabase = createClient(supabaseUrl, supabaseRoleKey, {
    auth: {
        autoRefreshToken: false,
        persistSession: false,
    },
});

export const create = async (username, userFullName) => {
    const { data, error } = await supabase.auth.admin.createUser({
        email: username,
        user_metadata: { name: userFullName, isCorbadoUser: true },
        email_confirm: true,
    });
    if (error) {
        console.log("Error from create user: ", error.message);
        return null;
    }
    console.log(data);
    return data;
};

export const findByEmail = async (email) => {
    var { data, error } = await supabase.rpc("get_user_id_by_email", {
        email: email,
    });
    if (error) {
        console.log("Error from get_user_id_by_email: ", error.message);
        return null;
    }
    console.log(data);
    if (data.length == 0) {
        // No user found
        return null;
    }
    const id = data[0].id;
    var { data, error } = await supabase.auth.admin.getUserById(id);
    if (error) {
        console.log("Error from getUserById: ", error.message);
        return null;
    }
    console.log(data);
    if (data.user == null) {
        // No user found
        return null;
    }
    return data.user;
};

export const verifyPassword = async (email, password) => {
    const { data, error } = await supabase.auth.signInWithPassword({
        email: email,
        password: password,
    });
    if (error) {
        console.log("Error from verifyPassword: ", error.message);
        return null;
    }
    console.log(data);
    return data;
};
```

## 8. Integrate Existing Users

### 8.1. Add Webhooks

In [step 1](#setup-supabase-backend), 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](http://localhost:19915/corbado-webhook), so we
create a controller for the route which handles Corbados webhooks.

There, we initialize the Corbado Node.js SDK using the project ID and API Secret from
[step 2](#configure-corbado-project). 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.

```javascript
// src/controllers/corbadoWebhookController.js

import * as UserService from "../services/userService.js";
import { Configuration, SDK } from "@corbado/node-sdk";

const projectID = process.env.PROJECT_ID;
const apiSecret = process.env.API_SECRET;
const config = new Configuration(projectID, apiSecret);
const corbado = new SDK(config);

async function getUserStatus(username) {
    const user = await UserService.findByEmail(username);
    const isCorbadoUser = user != null && user.user_metadata.isCorbadoUser;

    if (!user || isCorbadoUser) {
        return "not_exists";
    } else {
        return "exists";
    }
}

async function verifyPassword(username, password) {
    try {
        const res = await UserService.verifyPassword(username, password);
        if (!res) {
            return false;
        }
        return true;
    } catch (error) {
        console.log(error);
        return false;
    }
}

export const webhook = async (req, res) => {
    try {
        // Get the webhook action and act accordingly. Every CorbadoSDK
        // webhook has an action.
        let request;
        let response;

        switch (corbado.webhooks.getAction(req)) {
            // Handle the "authMethods" action which basically checks
            // if a user exists on your side/in your database.
            case corbado.webhooks.WEBHOOK_ACTION.AUTH_METHODS: {
                request = corbado.webhooks.getAuthMethodsRequest(req);
                // Now check if the given user/username exists in your
                // database and send status. Implement getUserStatus()
                // function below.
                const status = await getUserStatus(request.data.username);
                response = corbado.webhooks.getAuthMethodsResponse(status);
                res.json(response);
                break;
            }

            // Handle the "passwordVerify" action which basically checks
            // if the given username and password are valid.
            case corbado.webhooks.WEBHOOK_ACTION.PASSWORD_VERIFY: {
                request = corbado.webhooks.getPasswordVerifyRequest(req);

                // Now check if the given username and password is
                // valid. Implement verifyPassword() function below.
                const isValid = await verifyPassword(
                    request.data.username,
                    request.data.password,
                );
                response = corbado.webhooks.getPasswordVerifyResponse(isValid);
                res.json(response);
                break;
            }
            default: {
                return res.status(400).send("Bad Request");
            }
        }
    } catch (error) {
        // We expose the full error message here. Usually you would
        // not do this (security!) but in this case CorbadoSDK is the
        // only consumer of your webhook. The error message gets
        // logged at CorbadoSDK and helps you and us debugging your
        // webhook.
        console.log(error);

        // If something went wrong just return HTTP status
        // code 500. For successful requests CorbadoSDK always
        // expects HTTP status code 200. Everything else
        // will be treated as error.
        return res.status(500).send(error.message);
    }
};
```

### 8.2 Connect Local Instance to the Internet

Corbado will attempt to call our webhooks, but currently they are hosted locally on
[http://localhost:19915/corbado-webhook](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](https://docs.corbado.com/products/corbado-connect/connect-via-webhooks/corbado-cli).
Then execute

```bash
corbado login
```

This will prompt you for your project ID from [step 2](#configure-corbado-project) as well
as your CLI secret which you can find in our
[app](https://app.corbado.com/app/settings/credentials/cli-secret).

Afterwards execute

```bash
corbado subscribe http://localhost:19915
```

This will start the tunnel on port 19915.

## 9. Run the Supabase Passkeys Application

With

```bash
npm start
```

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

![Supabase Passkeys: Corbado Login Page](https://www.corbado.com/website-assets/64b05276c33982116af5f01c_webcomponent_a8977c2871.png)

![Supabase Passkeys: Corbado Profile Page](https://www.corbado.com/website-assets/64b052aa385222a4350a3ddf_webcomponent_logged_in_0fc6ee08e3.png)

If we login as the password-based user we created in [step 1](#setup-supabase-backend)
using our password, we are offered to
[create a passkey](https://www.corbado.com/blog/passkey-creation-best-practices) after successful password
authentication:

![Supabase Passkeys: Create new Passkey](https://www.corbado.com/website-assets/64b052cea4e542d25acf6775_webcomponent_append_passkey_d197924621.png)

## 10. 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!

```javascript
const getSupabaseClient = (userID) => {
    const payload = {
        userID,
        sub: userID,
        exp: Math.floor(Date.now() / 1000) + 60 * 60,
    };
    const token = jwt.sign(payload, process.env.SUPABASE_JWT_SECRET);

    const supabase = createClient(
        process.env.SUPABASE_URL,
        process.env.SUPABASE_API_KEY,
        {
            global: {
                headers: {
                    Authorization: `Bearer ${token}`,
                },
            },
        },
    );
    return supabase;
};
```

## 11. 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](https://app.corbado.com/app/settings/sessions).

![Supabase Passkeys: Corbado Session Management](https://www.corbado.com/website-assets/64afe88d4bba30c4a0686885_session_settings_41178949a5.png)

## 12. Conclusion: Passkeys in Supabase

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](https://docs.corbado.com/sessions/overview).

Due to clear documentation and a well-structured client, Supabase is very convenient and
easy to use. Although one must [watch](https://www.corbado.com/blog/how-to-use-passkeys-apple-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.

## Frequently Asked Questions

### How do Corbado webhooks enable existing Supabase password users to transition to passkeys?

Corbado webhooks handle two actions: `authMethods` (checking if a user exists in Supabase)
and `passwordVerify` (validating the user's existing password via the Supabase JavaScript
client). When an existing user successfully authenticates with their password, Corbado
then prompts them to register a passkey, gradually migrating the user base without forcing
an immediate switch.

### Which Supabase API key should I use when integrating with Corbado and why?

You should use the `service_role` API key to initialize the Supabase client in your
backend, as it grants admin-level access for operations like `auth.admin.createUser` and
`auth.admin.getUserById`. This key must never be exposed publicly. The JWT secret is used
separately in the backend to generate user-scoped tokens that enforce Row Level Security
policies.

### How do I make my local Corbado webhook endpoint publicly accessible during development?

Install the Corbado CLI and run `corbado subscribe http://localhost:19915` to create a
tunnel between Corbado's servers and your local instance. This allows Corbado to deliver
webhook requests to your local Node.js app without requiring a public deployment, and
requires your project ID and CLI secret from the Corbado developer panel.

### Why does Supabase need a custom SQL function for a passkey integration rather than using built-in client methods?

The Supabase JavaScript client does not expose a method to look up a user by email address
directly. A custom PostgreSQL function (`get_user_id_by_email`) must be created via the
Supabase SQL Editor and called through the client's `rpc()` method to resolve an email to
a user ID, which is a prerequisite for checking whether an authenticating user already
exists in the database.
