Aibo Platform docs

Signing users in

Aibo doesn't run your login. Your users sign in with whatever you already use, and your backend turns "this person is signed in" into a short-lived user token (au_…) for the platform. This page shows the pattern with Clerk, which is how the Aibo apps (tenant zero) do it. It also covers Clerk sign-in for the admin console.

The pattern

  1. The client signs the person in with your identity provider and gets a session token.
  2. The client sends that token to your backend.
  3. Your backend verifies it, then calls the platform with your API key:
    • PUT /v1/users/{externalId} creates or updates the user, with a display name;
    • POST /v1/users/{externalId}/tokens mints a user token.
  4. The client calls the platform with the user token, and asks your backend for a new one before it expires (default 1 h).

Pick an externalId that never changes for the person, such as your identity provider's user id. The Aibo apps use clerk:{clerkUserId}.

Example: a Clerk-backed token endpoint

This is the core of apps/app-backend (POST /auth/session):

ts
import { verifyToken } from '@clerk/backend';
import { AiboClient } from '@aibo/sdk';

const aibo = new AiboClient({ baseUrl: process.env.AIBO_URL, apiKey: process.env.AIBO_API_KEY });

export async function session(req: Request) {
  const clerkToken = req.headers.get('authorization')?.replace(/^Bearer /, '') ?? '';
  // Networkless with CLERK_JWT_KEY (PEM public key), or JWKS via CLERK_SECRET_KEY.
  const claims = await verifyToken(clerkToken, {
    jwtKey: process.env.CLERK_JWT_KEY,
    secretKey: process.env.CLERK_SECRET_KEY,
    authorizedParties: ['https://app.example.com'], // the origins your web app runs on
  }); // throws on a bad signature, expiry or azp

  const externalId = `clerk:${claims.sub}`;
  await aibo.users.upsert(externalId, { displayName: String(claims.name ?? 'Aibo user') });
  const { token, expiresAt } = await aibo.users.createToken(externalId, { ttlSeconds: 3600 });
  return Response.json({ token, expiresAt, externalId });
}

On the client, give the SDK a getToken that calls that endpoint with the Clerk session token, and cache the result until shortly before expiresAt:

ts
const client = new AiboClient({
  baseUrl: 'https://api.heyaibo.com',
  getToken: async () => {
    const res = await fetch('/auth/session', {
      method: 'POST',
      headers: { Authorization: `Bearer ${await clerk.session.getToken()}` },
    });
    return (await res.json()).token;
  },
});

Notes:

Admin console with Clerk

The admin console can sign admins in with Clerk instead of a password. The console still uses its own console session (ac_…) for every API call; Clerk only proves who the admin is.

Endpoint What it does
GET /v1/console/auth-config { passwordLogin, clerk }: which sign-in methods this deployment offers
POST /v1/console/clerk/exchange { clerkToken } Verifies the Clerk session token and returns a console session (status: "signed_in"), or { status: "needs_tenant", email }
POST /v1/console/signup { clerkToken, tenantName, tenantSlug } Creates the workspace (tenant) with the Clerk user as its first admin

On the first Clerk sign-in, the API links the Clerk user to the existing admin whose email equals the user's verified primary Clerk email. After that the link wins, even if the email changes later. An admin linked to one Clerk user can't be claimed by another (409), and an unverified email links nothing and creates nothing (403).

ts
const anon = new AiboClient({ baseUrl: process.env.AIBO_URL });
const res = await anon.console.clerkExchange({ clerkToken });
const session =
  res.status === 'signed_in'
    ? res
    : await anon.console.clerkSignup({ clerkToken, tenantName: 'Acme', tenantSlug: 'acme' });
const admin = new AiboClient({ baseUrl: process.env.AIBO_URL, consoleToken: session.token });

Email + password (POST /v1/console/login and password signup) works only while the deployment sets CONSOLE_PASSWORD_LOGIN=true. That is the default outside production; in production it defaults to off.

Configuration

Variable Where Purpose
CLERK_SECRET_KEY API, app-backend Verifies tokens via Clerk's JWKS and reads user profiles
CLERK_JWT_KEY API, app-backend Optional PEM public key: verifies tokens without calling Clerk
CLERK_AUTHORIZED_PARTIES API, app-backend Comma-separated origins allowed in azp
CLERK_PUBLISHABLE_KEY API, app-backend Optional; enables the issuer check
CONSOLE_PASSWORD_LOGIN API Console email + password login (default: on outside production)
AUTH_MODE app-backend clerk or dev. Dev mode (no Clerk) is refused in production

The Aibo apps take their publishable key from VITE_CLERK_PUBLISHABLE_KEY (web), EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY (iOS) and NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY (console).