Skip to main content

    Vercel Quickstart (Edge Runtime + Vercel AI SDK)

    Deploy a Behest-backed chat on Vercel with the Edge Runtime for sub-100ms cold starts and optional Vercel AI SDK (useChat) integration.

    Prerequisites: Next.js (App Router) deployed on Vercel, your existing auth (NextAuth/Clerk/Supabase/Auth0), a Behest project + key.

    This builds on the Next.js App Router quickstart. Two Vercel-specific variations: (A) run the token route on the Edge Runtime, (B) wire up Vercel AI SDK's useChat hook.


    1. Configure CORS (don't skip — silent killer)

    In Behest dashboard → Project → Settings → Allowed Origins, add every origin that will call Behest from a browser:

    http://localhost:3000
    https://your-app.vercel.app
    https://your-preview-*.vercel.app
    https://your-production-domain.com
    

    Click Save. Vercel preview deployments get unique subdomains (your-app-git-branch-team.vercel.app); add each branch pattern you'll preview from, or add a wildcard pattern if Behest Allowed Origins supports it (check the UI — today it expects exact matches, so list each one you'll actually hit).


    2. Install

    bash
    npm i @behest/client-ts@beta openai
    # Optional, for variation B:
    npm i ai

    .env.local (and set the same vars in Vercel → Project → Environment Variables):

    bash
    BEHEST_KEY=behest_sk_live_xxxxxxxx
    BEHEST_BASE_URL=https://your-slug.behest.app
    NEXT_PUBLIC_BEHEST_BASE_URL=https://your-slug.behest.app

    3. Token route on Edge Runtime

    The v1.5 SDK uses jose for signing — it works on the Edge Runtime. Add one line (export const runtime = "edge") and the route deploys as an Edge Function in Vercel:

    ts
    // app/api/behest/token/route.ts
    import { NextResponse } from "next/server";
    import { Behest } from "@behest/client-ts";
    import { auth } from "@/auth";
     
    export const runtime = "edge"; // Vercel Edge Function
     
    const behest = new Behest(); // reads BEHEST_KEY + BEHEST_BASE_URL
     
    export async function POST() {
      const session = await auth();
      if (!session?.user?.id)
        return new NextResponse("Unauthorized", { status: 401 });
      const result = await behest.auth.mint({ user_id: session.user.id, ttl: 900 });
      return NextResponse.json(result);
    }

    Why Edge here:

    • Token mint is trivially small (no DB, no Node-only deps).
    • Runs closer to the user; cold-start is ~10× faster than Node serverless.
    • Works on both apiKey mode (one HTTP call to Behest) and local-sign mode (zero outbound calls).

    If you use NextAuth with Prisma, keep this route on the Node runtime (Prisma isn't edge-safe). Remove the runtime = "edge" line.


    4a. Variation A — Browser-direct (simplest)

    Same pattern as the Next.js quickstart. Browser fetches the token, calls Behest directly with the OpenAI SDK. Keeps the server path at "vend a token, that's it":

    tsx
    // app/chat/ChatClient.tsx
    "use client";
    import { useRef, useState } from "react";
    import OpenAI from "openai";
     
    const BASE_URL = `${process.env.NEXT_PUBLIC_BEHEST_BASE_URL}/v1`;
     
    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;
      cached = await (await fetch("/api/behest/token", { method: "POST" })).json();
      return cached!;
    }
     
    export default function ChatClient() {
      const [messages, setMessages] = useState<{ role: string; content: string }[]>(
        []
      );
      const [input, setInput] = useState("");
      const abortRef = useRef<AbortController | null>(null);
     
      async function send() {
        abortRef.current = new AbortController();
        const next = [...messages, { role: "user" as const, content: input }];
        setMessages([...next, { role: "assistant", content: "" }]);
        setInput("");
     
        const { token, sessionId } = await getToken();
        const openai = new OpenAI({
          apiKey: token,
          baseURL: BASE_URL,
          dangerouslyAllowBrowser: true,
          defaultHeaders: { "X-Session-Id": sessionId },
        });
     
        const stream = await openai.chat.completions.create(
          { messages: next, stream: true },
          { signal: abortRef.current.signal }
        );
        for await (const chunk of stream) {
          const delta = chunk.choices[0]?.delta?.content ?? "";
          setMessages((m) => {
            const copy = [...m];
            const last = copy[copy.length - 1];
            copy[copy.length - 1] = {
              role: "assistant",
              content: last.content + delta,
            };
            return copy;
          });
        }
      }
     
      return (
        <div>
          {messages.map((m, i) => (
            <p key={i}>
              <b>{m.role}:</b> {m.content}
            </p>
          ))}
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && send()}
          />
          <button onClick={() => abortRef.current?.abort()}>Stop</button>
        </div>
      );
    }

    4b. Variation B — Vercel AI SDK useChat

    If you're already using ai's useChat hook (or want to), the cleanest path is a thin streaming proxy route that uses the Behest SDK server-side and returns a Vercel AI SDK-compatible stream. The browser never holds a token; your Edge route does.

    ts
    // app/api/chat/route.ts
    import { Behest } from "@behest/client-ts";
    import { OpenAIStream, StreamingTextResponse } from "ai";
    import { auth } from "@/auth";
     
    export const runtime = "edge";
     
    const behest = new Behest();
     
    export async function POST(req: Request) {
      const session = await auth();
      if (!session?.user?.id) return new Response("Unauthorized", { status: 401 });
     
      const { messages } = await req.json();
     
      // SDK auto-mints a per-user token keyed to this call's user_id.
      const response = await behest.chat.completions.create({
        messages,
        stream: true,
        user_id: session.user.id,
      });
     
      // Hand the OpenAI-compatible stream to Vercel's helper.
      return new StreamingTextResponse(
        OpenAIStream(response as unknown as Response)
      );
    }

    Then in a client component:

    tsx
    "use client";
    import { useChat } from "ai/react";
     
    export default function Chat() {
      const { messages, input, handleInputChange, handleSubmit, isLoading, stop } =
        useChat({ api: "/api/chat" });
     
      return (
        <form onSubmit={handleSubmit}>
          {messages.map((m) => (
            <div key={m.id}>
              <b>{m.role}:</b> {m.content}
            </div>
          ))}
          <input value={input} onChange={handleInputChange} />
          {isLoading && (
            <button type="button" onClick={stop}>
              Stop
            </button>
          )}
        </form>
      );
    }

    Tradeoffs of B vs A:

    A (browser-direct)B (Vercel AI SDK)
    Browser holds a JWT✅ yes (short-lived, per-user)❌ no
    Chat bandwidth through your servernoyes (every token flows through Edge)
    CORS config requiredyes (Behest Allowed Origins)no (browser only hits your domain)
    Works with useChat DX out of the boxcustom integration✅ drop-in
    Edge cost per tokenzero~one execution per message

    Most apps pick A for cost reasons; pick B if useChat ergonomics outweigh the pass-through cost, or if you need a server-side guard rail you can't run in the browser.


    5. Deploy

    bash
    git push

    Vercel auto-deploys. In the Vercel dashboard → Functions tab you should see api/behest/token (and api/chat if variation B) marked as "Edge" runtime.


    6. Verify

    1. Visit your preview deployment → sign in → send a message → reply appears.
    2. Behest dashboard → Usage: requests appear under your signed-in user's id.
    3. Vercel dashboard → Logs for the api/behest/token function: should show 200s, not 500s.
    4. CORS error? See troubleshooting in the Next.js quickstart.

    See also

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

    Learn more