Clerk → Behest
Map Clerk users to Behest users. Clerk's JWT templates make this especially clean — you can have Clerk issue tokens Behest accepts directly, with no intermediate mint call.
Option A — API-key mint (simplest)
Backend exchanges a Clerk session for a Behest JWT.
1. Route handler (Next.js)
// app/api/behest/token/route.ts
import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import { Behest } from "@behest/client-ts";
const behest = new Behest(); // reads BEHEST_KEY + BEHEST_BASE_URL from env
export async function POST() {
const { userId, sessionClaims } = await auth();
if (!userId) return new NextResponse("Unauthorized", { status: 401 });
const tier = (sessionClaims?.metadata as any)?.plan ?? 1;
try {
const { token, ttl, sessionId, expiresAt } = await behest.auth.mint({
user_id: userId,
tier,
ttl: 900,
});
return NextResponse.json({ token, ttl, sessionId, expiresAt });
} catch (err) {
return NextResponse.json({ error: String(err) }, { status: 500 });
}
}2. Client
import OpenAI from "openai";
type TokenBundle = {
token: string;
ttl: number;
sessionId: string;
expiresAt: number;
};
let cached: TokenBundle | null = null;
async function getBehestToken(): Promise<TokenBundle> {
const now = Math.floor(Date.now() / 1000);
if (cached && cached.expiresAt - now > 60) return cached;
const r = await fetch("/api/behest/token", { method: "POST" });
if (!r.ok) throw new Error(`token fetch failed: ${r.status}`);
cached = (await r.json()) as TokenBundle;
return cached;
}
export async function getOpenAI() {
const { token, sessionId } = await getBehestToken();
return new OpenAI({
apiKey: token,
baseURL: `${process.env.NEXT_PUBLIC_BEHEST_BASE_URL}/v1`,
dangerouslyAllowBrowser: true,
defaultHeaders: { "X-Session-Id": sessionId },
});
}Option B — Clerk JWT template (zero backend hop)
Behest can verify Clerk-signed JWTs directly. Perfect for static sites and mobile.
1. Create a JWT template in Clerk
Clerk dashboard → JWT Templates → New template → name it behest.
Template body:
{
"iss": "https://<your-clerk-frontend-api>",
"tid": "<your-behest-tenant-id>",
"pid": "<your-behest-project-id>",
"uid": "{{user.id}}",
"tier": "{{user.public_metadata.plan}}",
"email": "{{user.primary_email_address}}"
}Token lifetime: 15 minutes.
2. Tell Behest to trust Clerk
Behest dashboard → Project → Auth → Add trusted issuer:
- Issuer:
https://<your-clerk-frontend-api> - JWKS URL:
https://<your-clerk-frontend-api>/.well-known/jwks.json - Claims mapping:
sub→uid,public_metadata.plan→tier
3. Client uses Clerk's getToken
import { useAuth } from "@clerk/nextjs";
import { useMemo } from "react";
import OpenAI from "openai";
export function useOpenAI() {
const { getToken } = useAuth();
return useMemo(
() => async () => {
const token = await getToken({ template: "behest" });
if (!token) throw new Error("not signed in");
return new OpenAI({
apiKey: token,
baseURL: `${process.env.NEXT_PUBLIC_BEHEST_BASE_URL}/v1`,
dangerouslyAllowBrowser: true,
});
},
[getToken]
);
}That's it — no /api/behest/token route, no Behest key on your server. Clerk is your token minter; Behest trusts the signature.
Syncing tiers
When a user upgrades:
- Your Stripe webhook → update Clerk
publicMetadata.plan. - Next
getToken()returns a JWT with the new tier. - Behest enforces new limits instantly.
For Option A, same idea — the mint function reads sessionClaims.metadata.plan on every call.
RBAC / organizations
Clerk orgs → Behest tid? Not 1:1 — Behest tenants are account-level. Usually you keep one Behest tenant per Clerk instance and distinguish orgs via pid (one project per org) or via the org_id claim that you stuff into JWT and your app reads client-side.
For usage reporting grouped by org, pass an org-scoped user_id at mint time:
user_id: `${orgId}:${userId}`;Then usage queries group naturally.
Logout / revocation
- Option A: nothing to do — Behest JWT expires at
ttl. If you need instant revoke, reducettlto 60s. - Option B: deleting a Clerk session invalidates the template immediately (Clerk's JWKS stops serving the key). Behest picks up within 5 min (JWKS cache TTL).
See also
- Auth modes — local signing vs mint
- Next.js quickstart
- NextAuth integration