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.sql2create 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-time16alter publication supabase_realtime add table ai_content;17 18-- Row-level security19alter table ai_content enable row level security;20 21create policy "Users can view their content"22 on ai_content for select23 using (metadata->>'user_id' = auth.uid()::text);Edge Function webhook handler
TYPESCRIPT
1// supabase/functions/modelriver-webhook/index.ts2import { 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 here16 const crypto = globalThis.crypto;17 // In production, use crypto.subtle for proper HMAC verification18 return signature.length > 0; // Simplified: see full verification below19}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 workflow51 if (type === "task.ai_generated" && callback_url) {52 try {53 const aiData = ai_response?.data ?? {};54 55 // 3. Write to Supabase56 const { data: record, error } = await supabase57 .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 record76 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 callback108 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-webhookSet secrets
Bash
supabase secrets set MODELRIVER_API_KEY=mr_live_YOUR_API_KEYsupabase secrets set MODELRIVER_WEBHOOK_SECRET=your_webhook_secretFrontend with Realtime
Listen for new records in real time using Supabase Realtime subscriptions:
TYPESCRIPT
1// React example2import { 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 content15 const channel = supabase16 .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
- Use Edge Functions for webhooks: Globally distributed, low-latency webhook processing.
- Enable Realtime on your table: Clients get instant updates without polling.
- Use Row-Level Security: Restrict access to AI-generated content based on user identity.
- Use service role key in Edge Functions: Bypass RLS for server-side writes.
- Store metadata as JSONB: Flexible schema for varying AI response shapes.
Next steps
- PlanetScale event-driven guide: MySQL alternative
- Supabase vector integration: pgvector for embeddings
- Event-driven AI overview: Architecture and flow