Documentation

Event-driven AI with Supabase

Receive AI webhooks in Supabase Edge Functions, write structured data to Postgres, and stream results to clients with Supabase Realtime.

Overview

Supabase combines Postgres, real-time subscriptions, and Edge Functions in one platform. This makes it ideal for event-driven AI: your Edge Function receives the webhook, writes AI data directly to Postgres, and Supabase Realtime pushes the changes to connected clients.

What you'll build:

  • A Supabase Edge Function that receives ModelRiver webhooks
  • Database schema for storing AI-generated content
  • Real-time subscription for instant frontend updates
  • Callback to ModelRiver with enriched database records

Database schema

SQL
1-- supabase/migrations/001_ai_content.sql
2create table ai_content (
3 id uuid default gen_random_uuid() primary key,
4 title text not null,
5 body text,
6 category text default 'general',
7 metadata jsonb default '{}',
8 channel_id text,
9 event_name text,
10 source text default 'modelriver',
11 created_at timestamptz default now(),
12 updated_at timestamptz default now()
13);
14 
15-- Enable real-time
16alter publication supabase_realtime add table ai_content;
17 
18-- Row-level security
19alter table ai_content enable row level security;
20 
21create policy "Users can view their content"
22 on ai_content for select
23 using (metadata->>'user_id' = auth.uid()::text);

Edge Function webhook handler

TYPESCRIPT
1// supabase/functions/modelriver-webhook/index.ts
2import { serve } from "https://deno.land/[email protected]/http/server.ts";
3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
4 
5const supabase = createClient(
6 Deno.env.get("SUPABASE_URL")!,
7 Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
8);
9 
10function verifySignature(payload: string, signature: string, secret: string): boolean {
11 const encoder = new TextEncoder();
12 const key = encoder.encode(secret);
13 
14 // Use Web Crypto API (available in Deno/Edge)
15 // For simplicity, using a synchronous comparison here
16 const crypto = globalThis.crypto;
17 // In production, use crypto.subtle for proper HMAC verification
18 return signature.length > 0; // Simplified: see full verification below
19}
20 
21serve(async (req) => {
22 if (req.method !== "POST") {
23 return new Response("Method not allowed", { status: 405 });
24 }
25 
26 const signature = req.headers.get("mr-signature") ?? "";
27 const rawBody = await req.text();
28 const webhookSecret = Deno.env.get("MODELRIVER_WEBHOOK_SECRET") ?? "";
29 
30 // 1. Verify signature (use proper HMAC-SHA256 in production)
31 const key = await crypto.subtle.importKey(
32 "raw",
33 new TextEncoder().encode(webhookSecret),
34 { name: "HMAC", hash: "SHA-256" },
35 false,
36 ["sign"]
37 );
38 const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(rawBody));
39 const expected = Array.from(new Uint8Array(sig))
40 .map((b) => b.toString(16).padStart(2, "0"))
41 .join("");
42 
43 if (expected !== signature) {
44 return new Response(JSON.stringify({ error: "Invalid signature" }), { status: 401 });
45 }
46 
47 const payload = JSON.parse(rawBody);
48 const { type, event, ai_response, callback_url, customer_data, channel_id } = payload;
49 
50 // 2. Handle event-driven workflow
51 if (type === "task.ai_generated" && callback_url) {
52 try {
53 const aiData = ai_response?.data ?? {};
54 
55 // 3. Write to Supabase
56 const { data: record, error } = await supabase
57 .from("ai_content")
58 .insert({
59 title: aiData.title ?? "Untitled",
60 body: aiData.description ?? aiData.body ?? "",
61 category: customer_data?.category ?? "general",
62 metadata: {
63 ...customer_data,
64 ai_model: payload.meta?.model,
65 ai_provider: payload.meta?.provider,
66 },
67 channel_id,
68 event_name: event,
69 })
70 .select()
71 .single();
72 
73 if (error) throw error;
74 
75 // 4. Call back to ModelRiver with database record
76 const callbackResponse = await fetch(callback_url, {
77 method: "POST",
78 headers: {
79 Authorization: `Bearer ${Deno.env.get("MODELRIVER_API_KEY")}`,
80 "Content-Type": "application/json",
81 },
82 body: JSON.stringify({
83 data: {
84 ...aiData,
85 id: record.id,
86 saved_at: record.created_at,
87 supabase_url: `${Deno.env.get("SUPABASE_URL")}/rest/v1/ai_content?id=eq.${record.id}`,
88 },
89 task_id: `supabase_${record.id}`,
90 metadata: {
91 database: "supabase",
92 table: "ai_content",
93 record_id: record.id,
94 },
95 }),
96 });
97 
98 if (!callbackResponse.ok) {
99 throw new Error(`Callback failed: ${callbackResponse.status}`);
100 }
101 
102 return new Response(JSON.stringify({ received: true }), { status: 200 });
103 
104 } catch (error) {
105 console.error("Error:", error);
106 
107 // Send error callback
108 await fetch(callback_url, {
109 method: "POST",
110 headers: {
111 Authorization: `Bearer ${Deno.env.get("MODELRIVER_API_KEY")}`,
112 "Content-Type": "application/json",
113 },
114 body: JSON.stringify({
115 error: "processing_failed",
116 message: error.message,
117 }),
118 });
119 
120 return new Response(JSON.stringify({ received: true }), { status: 200 });
121 }
122 }
123 
124 return new Response(JSON.stringify({ received: true }), { status: 200 });
125});

Deploy the Edge Function

Bash
supabase functions deploy modelriver-webhook

Set secrets

Bash
supabase secrets set MODELRIVER_API_KEY=mr_live_YOUR_API_KEY
supabase secrets set MODELRIVER_WEBHOOK_SECRET=your_webhook_secret

Frontend with Realtime

Listen for new records in real time using Supabase Realtime subscriptions:

TYPESCRIPT
1// React example
2import { createClient } from "@supabase/supabase-js";
3import { useEffect, useState } from "react";
4 
5const supabase = createClient(
6 process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
8);
9 
10export function AiContentFeed() {
11 const [items, setItems] = useState<any[]>([]);
12 
13 useEffect(() => {
14 // Subscribe to new AI content
15 const channel = supabase
16 .channel("ai_content_changes")
17 .on(
18 "postgres_changes",
19 { event: "INSERT", schema: "public", table: "ai_content" },
20 (payload) => {
21 setItems((prev) => [payload.new, ...prev]);
22 }
23 )
24 .subscribe();
25 
26 return () => {
27 supabase.removeChannel(channel);
28 };
29 }, []);
30 
31 return (
32 <div>
33 {items.map((item) => (
34 <div key={item.id} className="p-4 border rounded mb-2">
35 <h3 className="font-bold">{item.title}</h3>
36 <p>{item.body}</p>
37 <span className="text-xs text-gray-500">{item.created_at}</span>
38 </div>
39 ))}
40 </div>
41 );
42}

Best practices

  1. Use Edge Functions for webhooks: Globally distributed, low-latency webhook processing.
  2. Enable Realtime on your table: Clients get instant updates without polling.
  3. Use Row-Level Security: Restrict access to AI-generated content based on user identity.
  4. Use service role key in Edge Functions: Bypass RLS for server-side writes.
  5. Store metadata as JSONB: Flexible schema for varying AI response shapes.

Next steps