Supabase Edge Functions client for content moderation. App Store compliance at the edge.
npm install @vettly/supabaseSupabase Edge Functions for App Store Guideline 1.2 compliance. Deno-compatible client with fetch-based transport for serverless environments.
Apple requires every iOS app with user-generated content to implement four things. This package handles all four in Supabase Edge Functions:
| Requirement | Guideline | Supabase Integration |
|-------------|-----------|----------------------|
| Content filtering | 1.2.1 | createModerationHandler(), client.check() |
| User reporting | 1.2.2 | Fetch to REST API (POST /v1/reports) |
| User blocking | 1.2.3 | Fetch to REST API (POST /v1/blocks) |
| Audit trail | — | result.decisionId on every decision |
``typescript
// supabase/functions/comments/index.ts
import { createModerationHandler } from '@vettly/supabase'
Deno.serve(createModerationHandler({
policyId: 'app-store',
onBlock: (result) => new Response(
JSON.stringify({ error: 'Blocked', decisionId: result.decisionId }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
)
}))
`
1. Sign up at vettly.dev
2. Go to Dashboard > API Keys
3. Create and copy your key
4. See Environment Setup below for Supabase-specific configuration
Supabase Edge Functions run on Deno at the edge. This package provides:
- Deno-compatible - Pure fetch-based transport, no Node.js dependencies
- Edge-optimized - Minimal cold start, works in Supabase's 2ms startup
- Handler utilities - One-liner Edge Function moderation
- Full audit trail - Every decision recorded with unique ID
`bash`
npm install @vettly/supabase
`typescript`
import { moderate } from 'npm:@vettly/supabase'
---
The fastest way to add moderation to an Edge Function:
`typescript
// supabase/functions/comments/index.ts
import { createModerationHandler } from '@vettly/supabase'
Deno.serve(createModerationHandler({
policyId: 'community-safe',
onBlock: (result) => new Response(
JSON.stringify({
error: 'Content blocked',
decisionId: result.decisionId,
categories: result.categories.filter(c => c.triggered)
}),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
)
}))
`
---
For more control:
`typescript
// supabase/functions/posts/index.ts
import { createClient } from '@vettly/supabase'
const vettly = createClient({
apiKey: Deno.env.get('VETTLY_API_KEY')!
})
Deno.serve(async (req) => {
const { content, userId } = await req.json()
// Check content
const result = await vettly.check(content, {
policyId: 'community-safe',
metadata: { userId }
})
if (result.action === 'block') {
return new Response(
JSON.stringify({
error: 'Content blocked',
decisionId: result.decisionId
}),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
)
}
// Content allowed - proceed with your logic
// Store result.decisionId for audit trail
return new Response(
JSON.stringify({ success: true, decisionId: result.decisionId }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
`
---
Create a configured Vettly client.
`typescript
import { createClient } from '@vettly/supabase'
const client = createClient({
apiKey: Deno.env.get('VETTLY_API_KEY')!,
apiUrl: 'https://api.vettly.dev' // optional
})
`
#### client.check(content, options)
Check text content against a policy.
`typescript
const result = await client.check('User-generated text', {
policyId: 'community-safe',
metadata: { userId: 'user_123' }
})
console.log(result.action) // 'allow' | 'warn' | 'flag' | 'block'
console.log(result.decisionId) // UUID for audit trail
console.log(result.categories) // Array of { category, score, triggered }
`
#### client.checkImage(imageUrl, options)
Check an image against a policy.
`typescript
// From URL
const result = await client.checkImage(
'https://cdn.example.com/image.jpg',
{ policyId: 'strict' }
)
// From base64
const result = await client.checkImage(
'data:image/jpeg;base64,/9j/4AAQ...',
{ policyId: 'strict' }
)
`
---
Quick moderation without creating a client. Uses VETTLY_API_KEY environment variable.
`typescript
import { moderate } from '@vettly/supabase'
const result = await moderate('User content', { policyId: 'default' })
if (result.action === 'block') {
// Handle blocked content
}
`
---
Create an Edge Function handler with built-in moderation.
`typescript
import { createModerationHandler } from '@vettly/supabase'
Deno.serve(createModerationHandler({
// Required
policyId: 'community-safe',
// Optional: field path in JSON body (default: 'content')
field: 'content',
// Optional: custom block response
onBlock: (result) => new Response(
JSON.stringify({ error: 'Blocked', decisionId: result.decisionId }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
),
// Optional: handler for allowed content
onAllow: async (req, result) => {
const body = await req.json()
// Your business logic here
// result.decisionId available for audit trail
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
})
}
}))
`
---
Wrap an existing Edge Function with moderation.
`typescript
import { withModeration } from '@vettly/supabase'
async function myHandler(req: Request): Promise
const body = await req.json()
// Your existing logic
await db.posts.create({ content: body.content })
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
})
}
Deno.serve(withModeration(myHandler, {
policyId: 'community-safe',
field: 'content'
}))
`
---
All moderation methods return:
`typescript`
interface ModerationResult {
decisionId: string // UUID for audit trail
safe: boolean // True if content passes
flagged: boolean // True if flagged for review
action: 'allow' | 'warn' | 'flag' | 'block'
categories: Array<{
category: string // e.g., 'hate_speech', 'harassment'
score: number // 0.0 to 1.0
triggered: boolean // True if threshold exceeded
}>
latency: number // Response time in ms
}
---
1. Go to your project settings
2. Navigate to Edge Functions > Secrets
3. Add VETTLY_API_KEY with your API key
Create .env.local in your Supabase project:
`bash`
VETTLY_API_KEY=sk_live_...
Or set in supabase/functions/.env:
`bash`
VETTLY_API_KEY=sk_live_...
---
`typescript
// supabase/functions/comments/index.ts
import { createClient } from '@vettly/supabase'
import { createClient as createSupabase } from '@supabase/supabase-js'
const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })
const supabase = createSupabase(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
Deno.serve(async (req) => {
if (req.method !== 'POST') {
return new Response('Method not allowed', { status: 405 })
}
const { content, postId, userId } = await req.json()
// Moderate content
const result = await vettly.check(content, {
policyId: 'comments',
metadata: { postId, userId }
})
if (result.action === 'block') {
return new Response(
JSON.stringify({
error: 'Comment blocked',
decisionId: result.decisionId
}),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
)
}
// Save comment with audit trail
const { data, error } = await supabase
.from('comments')
.insert({
content,
post_id: postId,
user_id: userId,
moderation_decision_id: result.decisionId,
moderation_action: result.action
})
.select()
.single()
if (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
return new Response(
JSON.stringify(data),
{ headers: { 'Content-Type': 'application/json' } }
)
})
`
`typescript
// supabase/functions/upload-image/index.ts
import { createClient } from '@vettly/supabase'
const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })
Deno.serve(async (req) => {
const formData = await req.formData()
const file = formData.get('file') as File
// Convert to base64 for moderation
const buffer = await file.arrayBuffer()
const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)))
const dataUri = data:${file.type};base64,${base64}
// Check image
const result = await vettly.checkImage(dataUri, { policyId: 'images' })
if (result.action === 'block') {
return new Response(
JSON.stringify({
error: 'Image rejected',
decisionId: result.decisionId,
categories: result.categories.filter(c => c.triggered)
}),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
)
}
// Proceed with upload...
// Store result.decisionId with the image record
return new Response(
JSON.stringify({ success: true, decisionId: result.decisionId }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
`
`typescript
// supabase/functions/moderation-webhook/index.ts
Deno.serve(async (req) => {
const signature = req.headers.get('x-vettly-signature')
const payload = await req.text()
// Verify webhook signature
// (implement verification as shown in main SDK docs)
const event = JSON.parse(payload)
switch (event.type) {
case 'decision.blocked':
// Notify moderators
await notifySlack(Content blocked: ${event.data.decisionId})
break
case 'decision.flagged':
// Add to review queue
await addToReviewQueue(event.data)
break
}
return new Response('OK')
})
`
---
`typescript
import { createClient } from '@vettly/supabase'
const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })
Deno.serve(async (req) => {
try {
const { content } = await req.json()
const result = await vettly.check(content, { policyId: 'default' })
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
// Log error but fail open (allow content through)
console.error('Moderation error:', error)
return new Response(
JSON.stringify({ warning: 'Moderation unavailable', allowed: true }),
{ headers: { 'Content-Type': 'application/json' } }
)
}
})
`
---
Full TypeScript support with Deno:
`typescript
import { createClient, type ModerationResult } from '@vettly/supabase'
const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })
Deno.serve(async (req: Request): Promise
const { content }: { content: string } = await req.json()
const result: ModerationResult = await vettly.check(content, {
policyId: 'community-safe'
})
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
})
})
``
---
- vettly.dev - Sign up
- docs.vettly.dev - Documentation
- Supabase Edge Functions - Supabase docs
- @vettly/sdk - Core SDK