npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vettly/supabase

v0.1.5

Published

Supabase Edge Functions client for content moderation. Content moderation at the edge.

Readme

@vettly/supabase

Supabase Edge Functions for UGC moderation. Deno-compatible client with fetch-based transport for serverless environments.

UGC Moderation Essentials

Apps with user-generated content need four things to stay compliant and keep users safe. This package handles all four in Supabase Edge Functions:

| Requirement | Supabase Integration | |-------------|----------------------| | Content filtering | createModerationHandler(), client.check() | | User reporting | Fetch to REST API (POST /v1/reports) | | User blocking | Fetch to REST API (POST /v1/blocks) | | Audit trail | result.decisionId on every decision |

// 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' } }
  )
}))

Get Your API Key

  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

Why Edge-Native?

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

Installation

npm (for bundling)

npm install @vettly/supabase

Deno (direct import)

import { moderate } from 'npm:@vettly/supabase'

Quick Start - One-Liner

The fastest way to add moderation to an Edge Function:

// 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' } }
  )
}))

Quick Start - Manual Integration

For more control:

// 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' } }
  )
})

API Reference

createClient(config)

Create a configured Vettly client.

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.

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.

// 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' }
)

moderate(content, options)

Quick moderation without creating a client. Uses VETTLY_API_KEY environment variable.

import { moderate } from '@vettly/supabase'

const result = await moderate('User content', { policyId: 'default' })

if (result.action === 'block') {
  // Handle blocked content
}

createModerationHandler(config)

Create an Edge Function handler with built-in moderation.

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' }
    })
  }
}))

withModeration(handler, config)

Wrap an existing Edge Function with moderation.

import { withModeration } from '@vettly/supabase'

async function myHandler(req: Request): Promise<Response> {
  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'
}))

Response Format

All moderation methods return:

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
}

Environment Setup

Supabase Dashboard

  1. Go to your project settings
  2. Navigate to Edge Functions > Secrets
  3. Add VETTLY_API_KEY with your API key

Local Development

Create .env.local in your Supabase project:

VETTLY_API_KEY=vettly_live_...

Or set in supabase/functions/.env:

VETTLY_API_KEY=vettly_live_...

Examples

Comments API

// 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' } }
  )
})

Image Upload Moderation

// 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' } }
  )
})

Webhook Handler

// 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')
})

Error Handling

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' } }
    )
  }
})

TypeScript Support

Full TypeScript support with Deno:

import { createClient, type ModerationResult } from '@vettly/supabase'

const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })

Deno.serve(async (req: Request): Promise<Response> => {
  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' }
  })
})

Links