Skip to main content

    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

    bash
    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.com

    BEHEST_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

    bash
    supabase functions new behest-token
    ts
    // 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:

    bash
    supabase functions deploy behest-token

    4. Call from the browser (framework-agnostic)

    ts
    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

    1. Sign in via Supabase auth → call ask("Hi!") → reply appears.
    2. Behest dashboard → Usage: request shows under the Supabase user's UUID.
    3. 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_ORIGIN secret matches your frontend origin exactly, and your Edge Function replies to OPTIONS preflight.
    • 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 curl but not the browser → always CORS. curl doesn'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

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

    Learn more