Skip to main content

    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)

    ts
    // 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

    ts
    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 TemplatesNew template → name it behest.

    Template body:

    json
    {
      "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 → AuthAdd 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

    tsx
    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:

    1. Your Stripe webhook → update Clerk publicMetadata.plan.
    2. Next getToken() returns a JWT with the new tier.
    3. 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:

    ts
    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, reduce ttl to 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

    Enterprise Token FinOps: Enforce hard budgets and attribute costs per session.

    Learn more