Skip to content
AI Infrastructure
AI Infrastructure12 min read0 views

Build a Supabase-Backed Voice Agent with RLS and Edge Functions

Use Supabase Auth, RLS-protected Postgres, and Deno Edge Functions to ship a multi-tenant voice agent. Real working SQL policies and TS code.

TL;DR — Supabase gives you Postgres + RLS + Auth + Deno Edge Functions in one box. Add OpenAI Realtime via the Edge Function as a WebSocket relay, and you have a multi-tenant voice agent in an afternoon.

What you'll build

A Supabase project with two tables (calls, messages), per-tenant RLS, an Edge Function that mints ephemeral OpenAI tokens, and a second Edge Function that relays Realtime traffic. Each call inserts rows that downstream LiveView/React clients see in real time.

Prerequisites

  1. Supabase project (free tier is enough).
  2. supabase CLI 1.180+.
  3. OPENAI_API_KEY set as a secret: supabase secrets set OPENAI_API_KEY=....
  4. Auth provider configured (we'll use email/magic link).
  5. Familiarity with SQL and Postgres RLS.

Architecture

flowchart LR
  B[Browser w/ Supabase JWT] -- WS --> EF[Edge Fn: voice-relay]
  EF -- WS --> O[OpenAI Realtime]
  EF -- insert --> PG[(Postgres + RLS)]
  PG -- realtime --> B

Step 1 — Schema with RLS

```sql create table calls ( id uuid primary key default gen_random_uuid(), user_id uuid references auth.users (id) not null, started_at timestamptz default now(), ended_at timestamptz );

create table messages ( id bigserial primary key, call_id uuid references calls (id) on delete cascade, role text check (role in ('user','assistant','system')), text text, inserted_at timestamptz default now() );

alter table calls enable row level security; alter table messages enable row level security;

create policy "owner reads calls" on calls for select using (user_id = auth.uid());

create policy "owner inserts calls" on calls for insert with check (user_id = auth.uid());

create policy "owner reads messages" on messages for select using ( exists (select 1 from calls c where c.id = messages.call_id and c.user_id = auth.uid()) ); ```

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →

Step 2 — Edge Function: mint ephemeral key

supabase/functions/mint-key/index.ts:

```typescript import { createClient } from "jsr:@supabase/supabase-js@2";

Deno.serve(async (req) => { const auth = req.headers.get("Authorization") ?? ""; const sb = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, { global: { headers: { Authorization: auth } } } ); const { data: { user } } = await sb.auth.getUser(); if (!user) return new Response("unauthorized", { status: 401 });

const r = await fetch("https://api.openai.com/v1/realtime/sessions", { method: "POST", headers: { Authorization: "Bearer " + Deno.env.get("OPENAI_API_KEY"), "Content-Type": "application/json", }, body: JSON.stringify({ model: "gpt-4o-realtime-preview-2025-06-03", voice: "alloy", }), }); return new Response(await r.text(), { headers: { "content-type": "application/json" } }); }); ```

Deploy: supabase functions deploy mint-key.

Step 3 — Edge Function: voice relay with persistence

```typescript import { createClient } from "jsr:@supabase/supabase-js@2";

Deno.serve(async (req) => { if (req.headers.get("upgrade") !== "websocket") return new Response("ws only", { status: 400 }); const { socket, response } = Deno.upgradeWebSocket(req);

const sb = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, );

let callId: string | null = null; const oai = new WebSocket( "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2025-06-03", ["realtime", "openai-insecure-api-key." + Deno.env.get("OPENAI_API_KEY"), "openai-beta.realtime-v1"]);

oai.onmessage = async (e) => { socket.send(e.data); const msg = JSON.parse(e.data); if (msg.type === "response.text.done" && callId) { await sb.from("messages").insert({ call_id: callId, role: "assistant", text: msg.text, }); } };

socket.onmessage = (e) => { const m = JSON.parse(e.data); if (m.type === "client.call_start") { sb.from("calls").insert({ user_id: m.user_id }).select().single() .then(({ data }) => { callId = data?.id; }); } else { oai.send(e.data); } }; socket.onclose = () => oai.close(); return response; }); ```

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

Step 4 — Realtime subscription in the browser

```typescript import { createClient } from "@supabase/supabase-js"; const sb = createClient(URL, ANON);

sb.channel("messages") .on("postgres_changes", { event: "INSERT", schema: "public", table: "messages" }, (p) => render(p.new)) .subscribe(); ```

Because RLS scopes the subscription to the user's own call_id rows, no extra filtering needed.

Step 5 — Set auth.uid() for transactional writes

For multi-statement Edge Function writes that need RLS, you must set the JWT explicitly:

```sql select set_config('request.jwt.claim.sub', '...uuid...', true); ```

Or use the user-scoped client (auth header forwarded), and Supabase does it for you.

Common pitfalls

  • Service-role inserts bypass RLS — don't use it for user-tied data without re-checking.
  • Edge Function 60s walltime cap — for long calls, return early and let the client reconnect.
  • Forgetting auth.uid() in policies — global table read.
  • WebSocket subprotocol leaks API key — mint ephemeral and POST it instead.

How CallSphere does this in production

We don't run on Supabase ourselves (we run our own Postgres on a 72.62 box with 115+ tables across 6 verticals), but we recommend Supabase for our affiliate developer-tier customers — under $1k MRR, RLS-protected, deploys in an hour. See /pricing for the equivalent CallSphere plans.

FAQ

Can I run all this without my own server? Yes — Edge Functions cover both the relay and the token issuance.

RLS performance? Negligible — Postgres compiles policies into the query plan.

What about HIPAA? Supabase has a BAA on Pro+; CallSphere has it on Pro+ too.

Can I use pgvector? Yes — Supabase ships pgvector for RAG.

Cost at 10k calls/day? ~$25 Supabase + your OpenAI bill.

Sources

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.