Skip to main content

    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

    bash
    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_id and tier MUST 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.

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

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

    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).

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

    bash
    npx tsx src/server.ts
    # /api/token requires an authenticated session cookie/header — hit it from your app, not curl.

    Next steps

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

    Learn more