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
- The client signs the person in with your identity provider and gets a session token.
- The client sends that token to your backend.
- 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}/tokensmints a user token.
- 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):
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:
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:
azp(authorized party). Clerk puts the page origin of a browser session inazp. Check it against your web origins. Native mobile sessions have noazp.- Profile claims. Clerk session tokens carry the user id (
sub) but not the email or name by default. Add them under Sessions → Customize session token (for example"name": "{{user.full_name}}","email": "{{user.primary_email_address}}"), or look the user up with Clerk's Backend API. - One person, one
externalId. Signing in on another device returns the sameexternalId, so the person sees the same agents and chats everywhere.
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).
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).