Supabase Edge Functions Quickstart
Use a Supabase Edge Function (Deno) as your token-mint backend — no Node server, no Vercel, no Next.js required. Your frontend can be anything: React SPA, SvelteKit, native mobile, even a plain HTML file.
Prerequisites: a Supabase project with auth enabled, Supabase CLI (
npm i -g supabase), a Behest project + key.
1. Configure CORS (don't skip — silent killer)
In Behest dashboard → Project → Settings → Allowed Origins, add every browser origin that will call Behest:
http://localhost:3000
http://localhost:5173
https://your-app.com
Click Save. Without this, every browser call fails with a CORS error before reaching your Edge Function. Protocol + host + port must match exactly; no trailing slash.
2. Store secrets in Supabase
supabase secrets set BEHEST_KEY=behest_sk_live_xxxxxxxxxxxx
supabase secrets set BEHEST_BASE_URL=https://your-slug.behest.app
supabase secrets set CORS_ORIGIN=https://your-app.comBEHEST_KEY never leaves the Edge Function. For local signing mode, use a behest_pk_... key instead and also set BEHEST_KID, BEHEST_TENANT_ID, BEHEST_PROJECT_ID — no code change.
3. Create the token function
supabase functions new behest-token// supabase/functions/behest-token/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { Behest } from "https://esm.sh/@behest/client-ts";
const behest = new Behest({
key: Deno.env.get("BEHEST_KEY")!,
baseUrl: Deno.env.get("BEHEST_BASE_URL")!,
});
const ALLOWED_ORIGIN = Deno.env.get("CORS_ORIGIN") ?? "";
function corsHeaders(): Record<string, string> {
return {
"Access-Control-Allow-Origin": ALLOWED_ORIGIN,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "authorization, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders() });
}
// Verify the caller's Supabase session — the Supabase JWT in Authorization is
// the source of user identity. Never trust a body-supplied user_id.
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{
global: {
headers: { Authorization: req.headers.get("Authorization") ?? "" },
},
}
);
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return new Response("Unauthorized", {
status: 401,
headers: corsHeaders(),
});
}
// Optional: look up tier from your profiles table
const { data: profile } = await supabase
.from("profiles")
.select("plan")
.eq("id", user.id)
.single();
try {
const result = await behest.auth.mint({
user_id: user.id,
tier: profile?.plan ?? 1,
ttl: 900,
});
return new Response(JSON.stringify(result), {
status: 200,
headers: { ...corsHeaders(), "Content-Type": "application/json" },
});
} catch (err) {
return new Response(String(err), { status: 500, headers: corsHeaders() });
}
});Deploy:
supabase functions deploy behest-token4. Call from the browser (framework-agnostic)
import OpenAI from "openai";
import { supabase } from "./supabase"; // your existing createClient(...)
type TokenBundle = {
token: string;
ttl: number;
sessionId: string;
expiresAt: number;
};
let cached: TokenBundle | null = null;
async function getBehestToken(): Promise<TokenBundle> {
const now = Math.floor(Date.now() / 1000);
if (cached && cached.expiresAt - now > 60) return cached;
const {
data: { session },
} = await supabase.auth.getSession();
const r = await fetch(
`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/behest-token`,
{
method: "POST",
headers: { Authorization: `Bearer ${session?.access_token}` },
}
);
if (!r.ok) throw new Error(`token fetch failed: ${r.status}`);
cached = (await r.json()) as TokenBundle;
return cached;
}
export async function ask(question: string) {
const { token, sessionId } = await getBehestToken();
const openai = new OpenAI({
apiKey: token,
baseURL: `${import.meta.env.VITE_BEHEST_BASE_URL}/v1`,
dangerouslyAllowBrowser: true,
defaultHeaders: { "X-Session-Id": sessionId },
});
const res = await openai.chat.completions.create({
messages: [{ role: "user", content: question }],
});
return res.choices[0].message.content ?? "";
}Add VITE_BEHEST_BASE_URL=https://your-slug.behest.app to your frontend env.
5. Verify
- Sign in via Supabase auth → call
ask("Hi!")→ reply appears. - Behest dashboard → Usage: request shows under the Supabase user's UUID.
- Browser console CORS error? Jump to troubleshooting below.
Troubleshooting CORS
Open DevTools → Network → click the failed /v1/chat/completions (or /functions/v1/behest-token) request → Headers:
- Token request (Edge Function) fails → check
CORS_ORIGINsecret matches your frontend origin exactly, and your Edge Function replies toOPTIONSpreflight. - Chat request (Behest) fails with no
Access-Control-Allow-Origin→ origin isn't in Behest Allowed Origins. Fix in dashboard, save, retry. - Both work in
curlbut not the browser → always CORS.curldoesn't enforce it.
Two CORS boundaries to worry about: (a) browser → Supabase Edge Function (your function's headers), (b) browser → Behest (dashboard Allowed Origins). Both must match your frontend origin.
See also
- Lovable + Supabase quickstart — same pattern, pre-wired for Lovable.
- Supabase integration guide — RLS patterns, tier sync, JWKS-direct option.
- Error handling