Supabase + Vettly
Moderate user-generated content in your Supabase app.
Overview
This guide covers:
- Moderating content before inserting into Supabase
- Using Edge Functions for server-side moderation
- Database triggers for async moderation
Option 1: Client-Side with RLS
Moderate in your client before inserting:
ts
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
const vettly = new ModerationClient({
apiKey: process.env.NEXT_PUBLIC_VETTLY_API_KEY!
})
export async function createPost(content: string, userId: string) {
// 1. Moderate first
const modResult = await vettly.check({
content,
contentType: 'text'
})
if (modResult.action === 'block') {
throw new Error('Content not allowed')
}
// 2. Insert if allowed
const { data, error } = await supabase
.from('posts')
.insert({
content,
user_id: userId,
moderation_status: modResult.action,
moderation_id: modResult.decisionId
})
.select()
.single()
return data
}Option 2: Edge Functions (Recommended)
More secure - keeps API key server-side.
Create the Edge Function
bash
supabase functions new moderate-contentts
// supabase/functions/moderate-content/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const { content, contentType = 'text' } = await req.json()
// Call Vettly API
const modResponse = await fetch('https://api.vettly.dev/v1/check', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('VETTLY_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content, contentType })
})
const result = await modResponse.json()
return new Response(JSON.stringify(result), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
})Set the Secret
bash
supabase secrets set VETTLY_API_KEY=vettly_your_api_keyDeploy
bash
supabase functions deploy moderate-contentUse in Your App
ts
// Client-side
const { data, error } = await supabase.functions.invoke('moderate-content', {
body: { content: userMessage, contentType: 'text' }
})
if (data.action === 'block') {
alert('Content not allowed')
return
}
// Proceed with insert
await supabase.from('posts').insert({ content: userMessage })Option 3: Database Webhook
Moderate asynchronously after insert.
1. Create a status column
sql
alter table posts add column moderation_status text default 'pending';
alter table posts add column moderation_id text;2. Create webhook endpoint
ts
// pages/api/webhooks/supabase-moderate.ts (Next.js example)
import { ModerationClient } from '@nextauralabs/vettly-sdk'
import { createClient } from '@supabase/supabase-js'
const vettly = new ModerationClient({
apiKey: process.env.VETTLY_API_KEY!
})
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY! // Service key for admin access
)
export default async function handler(req, res) {
const { record } = req.body
// Moderate the content
const result = await vettly.check({
content: record.content,
contentType: 'text'
})
// Update the record
await supabase
.from('posts')
.update({
moderation_status: result.action,
moderation_id: result.decisionId
})
.eq('id', record.id)
// If blocked, optionally delete or hide
if (result.action === 'block') {
await supabase
.from('posts')
.update({ visible: false })
.eq('id', record.id)
}
res.status(200).json({ success: true })
}3. Configure Supabase webhook
In Supabase Dashboard → Database → Webhooks:
- Table:
posts - Events:
INSERT - URL:
https://yourapp.com/api/webhooks/supabase-moderate
Filtering Moderated Content
Only show approved content:
ts
// Show only allowed content
const { data: posts } = await supabase
.from('posts')
.select('*')
.eq('moderation_status', 'allow')
.order('created_at', { ascending: false })Or use Row Level Security:
sql
-- RLS policy to hide blocked content
create policy "Hide blocked content"
on posts for select
using (moderation_status != 'block');Image Moderation with Supabase Storage
ts
// 1. Upload to temp bucket
const { data: upload } = await supabase.storage
.from('temp-uploads')
.upload(`${userId}/${filename}`, file)
// 2. Get signed URL
const { data: urlData } = await supabase.storage
.from('temp-uploads')
.createSignedUrl(upload.path, 60)
// 3. Download and moderate
const response = await fetch(urlData.signedUrl)
const buffer = await response.arrayBuffer()
const base64 = Buffer.from(buffer).toString('base64')
const modResult = await vettly.check({
content: base64,
contentType: 'image'
})
// 4. Move to permanent storage if allowed
if (modResult.action !== 'block') {
await supabase.storage
.from('uploads')
.copy(`temp-uploads/${upload.path}`, `uploads/${upload.path}`)
}
// 5. Clean up temp
await supabase.storage
.from('temp-uploads')
.remove([upload.path])Real-time Moderation Status
Subscribe to moderation updates:
ts
// Subscribe to changes
supabase
.channel('post-moderation')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`
},
(payload) => {
if (payload.new.moderation_status === 'block') {
showNotification('Your post was removed for policy violation')
}
}
)
.subscribe()Next Steps
- Custom Policies - Configure moderation rules
- Webhooks - Async moderation notifications
- Dashboard - Review moderation decisions