NextAuth → Behest
NextAuth (Auth.js v5) is the most common auth library for Next.js App Router apps. Pair it with a Behest token route handler.
1. NextAuth config (standard)
// auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [Google],
session: { strategy: "jwt" },
callbacks: {
async session({ session, token }) {
// Expose a stable user id and tier to the client and to our token route
session.user.id = token.sub!;
(session.user as any).tier = (token as any).tier ?? "free";
return session;
},
async jwt({ token, user, trigger, session }) {
if (user) (token as any).tier = (user as any).tier ?? "free";
if (trigger === "update" && session?.tier)
(token as any).tier = session.tier;
return token;
},
},
});2. Behest token route handler
// app/api/behest/token/route.ts
import { auth } from "@/auth";
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 session = await auth();
if (!session?.user?.id)
return new NextResponse("Unauthorized", { status: 401 });
try {
const { token, ttl, sessionId, expiresAt } = await behest.auth.mint({
user_id: session.user.id,
tier: (session.user as any).tier ?? 1,
ttl: 900,
});
return NextResponse.json({ token, ttl, sessionId, expiresAt });
} catch (err) {
return NextResponse.json({ error: String(err) }, { status: 500 });
}
}3. Client helper
// lib/behestToken.ts
"use client";
import OpenAI from "openai";
type TokenBundle = {
token: string;
ttl: number;
sessionId: string;
expiresAt: number;
};
let cached: TokenBundle | null = null;
async function getToken(): 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 getToken();
return new OpenAI({
apiKey: token,
baseURL: `${process.env.NEXT_PUBLIC_BEHEST_BASE_URL}/v1`,
dangerouslyAllowBrowser: true,
defaultHeaders: { "X-Session-Id": sessionId },
});
}4. Tier updates without sign-out
After a Stripe webhook upgrades the user:
import { useSession } from "next-auth/react";
const { update } = useSession();
await update({ tier: "pro" }); // re-issues NextAuth JWT with new tier
// next call to /api/behest/token reads the new tierCombined with Behest's short JWT TTL (15 min), users get upgraded limits on their very next request.
5. Local signing option
Skip the Behest round-trip entirely. Swap BEHEST_KEY=behest_sk_live_... for BEHEST_KEY=behest_pk_... (a tenant RSA private key from dashboard → Keys → Signing) and add BEHEST_KID, BEHEST_TENANT_ID, BEHEST_PROJECT_ID to env. The SDK auto-detects the mode by key prefix — the route handler code above is unchanged.
See auth modes for when this pays off.
6. Edge runtime
The SDK uses jose for signing, which works on Vercel / Cloudflare edge runtimes.
export const runtime = "edge";Perfect for Vercel / Cloudflare Pages Functions / etc.
7. Middleware consideration
If you put NextAuth in middleware, keep the Behest token route outside middleware or ensure the middleware lets it through — it's called from authenticated clients and only needs the NextAuth cookie.
// middleware.ts
export const config = {
matcher: ["/((?!api/behest/token|_next/static|_next/image).*)"],
};