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
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):
BEHEST_KEY=behest_sk_live_xxxxxxxx
BEHEST_BASE_URL=https://your-slug.behest.app
NEXT_PUBLIC_BEHEST_BASE_URL=https://your-slug.behest.app3. 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:
// 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":
// 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.
// 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:
"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 server | no | yes (every token flows through Edge) |
| CORS config required | yes (Behest Allowed Origins) | no (browser only hits your domain) |
Works with useChat DX out of the box | custom integration | ✅ drop-in |
| Edge cost per token | zero | ~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
git pushVercel 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
- Visit your preview deployment → sign in → send a message → reply appears.
- Behest dashboard → Usage: requests appear under your signed-in user's id.
- Vercel dashboard → Logs for the
api/behest/tokenfunction: should show 200s, not 500s. - CORS error? See troubleshooting in the Next.js quickstart.
See also
- Next.js App Router quickstart — the Node-runtime version of all this.
- NextAuth integration — add NextAuth on Edge.
- Streaming UI — cancel, reconnect, typewriter effect.
- Error handling — 402 upgrade, 429 backoff.