Skip to content
BuildBase

Keep your existing sign-in

What works when your app already signs people in with Supabase, Clerk, Auth0, Firebase or its own code, and you want BuildBase's other modules.

Your app already signs people in, with Supabase, Clerk, Auth0, Firebase or code of its own, and you want BuildBase for teams, plans, credits or email. BuildBase does not read your provider's tokens: there is no setting that tells it to trust a Supabase, Clerk, Auth0 or Firebase sign-in, and no way to look a person up by your provider's user ID. Every BuildBase module works on a BuildBase user and a BuildBase session. This page says what that leaves you, so you can choose before anything changes.

Stop and ask before migrating

If you are a coding agent: when the app already signs people in, stop and ask the person you work for which path below they want. Do not replace a working login on your own. It is the one change that can lock every existing user out.

Three paths

PathPeople sign in withWhat BuildBase gives youStatus
1. Move your users to BuildBaseBuildBase's hosted sign-inEverything, including the React SDK's screens and hooksSupported end to end. The path for an app made in Lovable, Bolt, Replit or v0
2. Your provider as a sign-in buttonYour provider, through BuildBase's hosted pageEverything, as path 1Supported for a provider that speaks OpenID Connect
3. Keep your sign-in, call BuildBase from your serverYour provider, as todayTeams, plans, credits and email, called from your own backendWorks through the API below. Not a packaged mode: you write and own the bridge

1. Move your users to BuildBase

The command line tool reads the export Clerk, Auth0, Firebase or Supabase gives you and creates each person in BuildBase, with their email address, name, phone number, their old ID kept as externalId, and the Google, GitHub, Microsoft, Apple or LinkedIn sign-ins they used.

npx buildbase import --from supabase supabase-users.json --dry-run

Passwords come with them. The import stores each person's old password hash, never the password itself, and a plaintext password is refused. At their next sign-in, people type the password they always had: BuildBase checks it with the old system's algorithm (bcrypt, argon2, Django's PBKDF2 or Firebase's scrypt), then stores it the way it stores every password and drops the old hash. Nobody is asked to reset. Auth0 leaves password hashes out of its export; ask Auth0 support for them before you plan the move. Someone whose hash cannot be checked is imported with no password and signs in with a magic link, an email code, a social provider or Forgot password?.

Sessions do not come across. Everyone signs in once more, and adds a passkey or two-step sign-in again if they had one.

Import your users from another service walks through it from the export to the first sign-in, and Importing users is the reference.

2. Your provider as a sign-in button

A custom OpenID Connect provider puts one more button on BuildBase's hosted sign-in page, for a provider such as Auth0, Okta or Keycloak. People press it, sign in on your provider's page as they do today, and come back signed in to BuildBase. Your provider stays the place their password lives.

It still goes through BuildBase's hosted page and its redirect, so it is path 1's sign-in with your provider behind one button, not a silent hand-off. The provider must publish OpenID Connect discovery (or be plain OAuth 2.0 with a user info address). Whether Supabase, Clerk or Firebase can act as that kind of provider for your project is for their docs to say: BuildBase's guide is written and tested against a standard OIDC provider, not against those three. An organization has one custom provider, and a plan can lock it.

3. Keep your sign-in, call BuildBase from your server

Your provider keeps signing people in, and your backend, which already knows who is signed in, opens a BuildBase session for that person and calls BuildBase with it. Three calls, all with an org API key that stays on your server:

  1. Create the person in BuildBase once, with POST /api/users/import and a one-line file. Each answer row carries the BuildBase userId, also when the person is already there (skipped). Store it beside your provider's ID: BuildBase has no lookup by externalId.
  2. Open a session for them with POST /api/v1/public/token/exchange. The key must have the owner or admin role to name another user.
  3. Call BuildBase with that session, through the server SDK's withSession() or the x-session-id header.
server/buildbase-bridge.ts
import { randomUUID } from 'node:crypto';
import BuildBase from '@buildbase/sdk';

const serverUrl = process.env.BUILDBASE_SERVER_URL!; // https://api.console.buildbase.app
const orgId = process.env.BUILDBASE_ORG_ID!;
const apiToken = process.env.BUILDBASE_API_TOKEN!; // orgId:secret, owner or admin role

// 1. Once per person: create them in BuildBase, keep their BuildBase id.
export async function ensureBuildBaseUser(user: {
  id: string;
  email: string;
  name?: string;
}) {
  const line = JSON.stringify({
    email: user.email,
    emailVerified: true,
    name: user.name,
    externalId: user.id,
  });
  const form = new FormData();
  form.append('file', new Blob([line + '\n']), 'user.ndjson');
  form.append('source', 'supabase');
  const res = await fetch(`${serverUrl}/api/users/import`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${apiToken}` },
    body: form,
  });
  const { rows } = await res.json();
  if (!rows?.[0]?.userId)
    throw new Error(rows?.[0]?.message ?? 'import failed');
  return rows[0].userId as string; // store it with your user
}

// 2. Open a session for them. Reuse it for its life (here, one day).
export async function openSession(buildbaseUserId: string) {
  const res = await fetch(`${serverUrl}/api/v1/public/token/exchange`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      token: apiToken,
      userId: buildbaseUserId,
      expiresIn: 86400,
    }),
  });
  const { sessionId } = await res.json();
  return sessionId as string;
}

// 3. Call any module as that person.
export async function spendCredits(sessionId: string, workspaceId: string) {
  const bb = BuildBase({ serverUrl, orgId }).withSession(sessionId);
  return bb.credits.consume(workspaceId, {
    amount: 1,
    idempotencyKey: randomUUID(),
  });
}

With the session, the person can create and join workspaces, see their plan, spend credits and do what the server SDK lists. Some calls need only the key, with no session: granting credits, reading a named person's plan and limits (BuildBase({ apiToken }), bb.entitlements.get({ user: '<userId>' })) and emailing a user.

Every field and refusal of the exchange is in opening a session as one of your users.

Which to choose

  • An app made in an AI app builder, with few users so far: path 1. Everything in the AI app builders pages assumes BuildBase sign-in.
  • Your provider is an OIDC provider and people should keep its page: path 2.
  • The sign-in cannot move yet, and you need billing or teams now: path 3, knowing you own the bridge, and plan the move to path 1 or 2 when you can.

Things to watch out for

Path 3 leaves these to you, all checked in the code:

  • No automatic user. A person who signs up with your provider does not exist in BuildBase until step 1 runs; the exchange answers 404 user_not_found until then. They count toward your plan's end-user limit like anyone else.
  • No sign-up events. A person created this way emits user.imported, never user.registered, and gets no welcome or verification email. A workflow or webhook that waits for sign-ups does not run for them; listen for user.imported.
  • A powerful key on your server. Only an owner or admin key may open a session for another user, so a leak of it is a leak of every user. Keep it out of the browser and out of your AI builder's frontend code.
  • Never a recent sign-in. Nobody signed in to an exchanged session, so a change that asks the person to confirm it's them is refused with 403 reauth_required. It is listed among the person's sessions as opened by your team (method: 'app_team'), and a company that uses enterprise SSO can refuse these sessions for its people.
  • The React SDK's screens expect BuildBase's sign-in. SaaSOSProvider is built around the hosted sign-in. Its getSession option restores a session your app hands it, so passing the exchanged session there may run its hooks without the hosted page, but that is inferred from the SDK's types, not documented or tested. Build this path on the server SDK first.

Next steps