Node + Express Quickstart
Minimal Node backend that mints per-user JWTs. Pair with any browser or mobile client.
Prerequisites: Node 18+, a Behest project + API key + slug.
1. Configure CORS (only if a browser will call Behest directly)
If your frontend calls Behest from the browser with the minted JWT — the recommended pattern below — add every browser origin to Project → Settings → Allowed Origins in the dashboard:
http://localhost:3000
http://localhost:5173
https://your-app.com
Click Save. Skip this step if your Node backend is the only caller (server-to-server).
2. Install
mkdir behest-node && cd behest-node && npm init -y
npm i express @behest/client-ts@beta
npm i -D tsx typescript @types/express @types/node.env:
BEHEST_KEY=behest_sk_live_xxxxxxxxxxxx
BEHEST_BASE_URL=https://amber-fox-042.behest.app
3. Token endpoint
Security:
user_idandtierMUST be derived from a server-verified session (cookie, session JWT, or auth provider — see integrations/supabase.md, clerk.md, nextauth.md). Never take them from the request body, query string, or a client-controlled header — a caller who does that can spoof any user in your tenant.
// src/server.ts
import express from "express";
import "dotenv/config";
import { Behest } from "@behest/client-ts";
import { requireSignedInUser } from "./auth"; // your app's session check
const app = express();
app.use(express.json());
// One Behest instance per process — reuse across requests.
const behest = new Behest(); // reads BEHEST_KEY and BEHEST_BASE_URL from env
app.post("/api/behest/token", async (req, res) => {
// Derive user_id + tier from YOUR verified session — never from req.body.
const user = await requireSignedInUser(req);
if (!user) return res.status(401).send("Unauthorized");
try {
const { token, ttl, sessionId, expiresAt } = await behest.auth.mint({
user_id: user.id,
tier: user.plan ?? 1,
ttl: 900,
});
res.json({ token, ttl, sessionId, expiresAt });
} catch (err) {
res.status(500).json({ error: String(err) });
}
});
app.listen(8787, () => console.log("http://localhost:8787"));Local signing alternative
If BEHEST_KEY starts with behest_pk_ (a tenant RSA private key), the same code above switches to local-sign mode — no HTTP round-trip per mint, and you also need BEHEST_KID, BEHEST_TENANT_ID, BEHEST_PROJECT_ID in env. See auth-modes for the tradeoff.
4. Server-to-server chat
Use Behest directly from Node for RAG pipelines, webhooks, background jobs:
// src/chat.ts
import { Behest } from "@behest/client-ts";
const behest = new Behest(); // reads BEHEST_KEY + BEHEST_BASE_URL from env
export async function answerFor(userId: string, question: string) {
// Pass user_id on the chat call — the SDK auto-mints a per-user JWT for this request.
const stream = await behest.chat.completions.create({
messages: [{ role: "user", content: question }],
stream: true,
user_id: userId,
});
let full = "";
for await (const chunk of stream)
full += chunk.choices[0]?.delta?.content ?? "";
return full;
}5. Browser talks to Behest directly (recommended)
Your frontend fetches a JWT from /api/behest/token (step 2), then calls Behest directly with the JWT — no per-chat server hop. Kong handles CORS per project (configure allowed origins in dashboard → Project → Settings).
// Browser — tiny helper
async function fetchBehestToken() {
const r = await fetch("/api/behest/token", {
method: "POST",
credentials: "include",
});
if (!r.ok) throw new Error("token fetch failed");
return (await r.json()) as {
token: string;
ttl: number;
sessionId: string;
expiresAt: number;
};
}
// Browser — stream a chat completion
const { token, sessionId } = await fetchBehestToken();
const resp = await fetch(
`${import.meta.env.VITE_BEHEST_BASE_URL}/v1/chat/completions`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"X-Session-Id": sessionId,
},
body: JSON.stringify({ messages, stream: true }),
}
);Only add a server-side /api/chat streaming proxy if you need central logging or to inject server-only data into every prompt. In most apps, browser-direct is simpler and cheaper.
6. Run
npx tsx src/server.ts
# /api/token requires an authenticated session cookie/header — hit it from your app, not curl.