tasq/.claude/skills/moai-platform-supabase/modules/edge-functions.md

8.2 KiB

name description parent-skill version updated
edge-functions Serverless Deno functions at the edge with authentication and rate limiting moai-platform-supabase 1.0.0 2026-01-06

Edge Functions Module

Overview

Supabase Edge Functions are serverless functions running on the Deno runtime at the edge, providing low-latency responses globally.

Basic Edge Function

Function Structure

// supabase/functions/api/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) => {
  // Handle CORS preflight
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL')!,
      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
    )

    // Process request
    const body = await req.json()

    return new Response(
      JSON.stringify({ success: true, data: body }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }
})

Authentication

JWT Token Verification

serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // Verify JWT token
  const authHeader = req.headers.get('authorization')
  if (!authHeader) {
    return new Response(
      JSON.stringify({ error: 'Unauthorized' }),
      { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }

  const { data: { user }, error } = await supabase.auth.getUser(
    authHeader.replace('Bearer ', '')
  )

  if (error || !user) {
    return new Response(
      JSON.stringify({ error: 'Invalid token' }),
      { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }

  // User is authenticated, proceed with request
  return new Response(
    JSON.stringify({ success: true, user_id: user.id }),
    { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  )
})

Using User Context Client

Create a client that inherits user permissions:

serve(async (req) => {
  const authHeader = req.headers.get('authorization')!

  // Client with user's permissions (respects RLS)
  const supabaseUser = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: authHeader } } }
  )

  // This query respects RLS policies
  const { data, error } = await supabaseUser
    .from('projects')
    .select('*')

  return new Response(JSON.stringify({ data }))
})

Rate Limiting

Database-Based Rate Limiting

-- Rate limits table
CREATE TABLE rate_limits (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  identifier TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_rate_limits_lookup ON rate_limits(identifier, created_at);

Rate Limit Function

async function checkRateLimit(
  supabase: SupabaseClient,
  identifier: string,
  limit: number,
  windowSeconds: number
): Promise<boolean> {
  const windowStart = new Date(Date.now() - windowSeconds * 1000).toISOString()

  const { count } = await supabase
    .from('rate_limits')
    .select('*', { count: 'exact', head: true })
    .eq('identifier', identifier)
    .gte('created_at', windowStart)

  if (count && count >= limit) {
    return false
  }

  await supabase.from('rate_limits').insert({ identifier })
  return true
}

Usage in Edge Function

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // Get client identifier (IP or user ID)
  const identifier = req.headers.get('x-forwarded-for') || 'anonymous'

  // 100 requests per minute
  const allowed = await checkRateLimit(supabase, identifier, 100, 60)

  if (!allowed) {
    return new Response(
      JSON.stringify({ error: 'Rate limit exceeded' }),
      { status: 429, headers: corsHeaders }
    )
  }

  // Process request...
})

External API Integration

Webhook Handler

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // Verify webhook signature
  const signature = req.headers.get('x-webhook-signature')
  const body = await req.text()

  const expectedSignature = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(body + Deno.env.get('WEBHOOK_SECRET'))
  )

  if (!verifySignature(signature, expectedSignature)) {
    return new Response('Invalid signature', { status: 401 })
  }

  const payload = JSON.parse(body)

  // Process webhook
  await supabase.from('webhook_events').insert({
    type: payload.type,
    data: payload.data,
    processed: false
  })

  return new Response('OK', { status: 200 })
})

External API Call

serve(async (req) => {
  const { query } = await req.json()

  // Call external API
  const response = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'text-embedding-ada-002',
      input: query
    })
  })

  const data = await response.json()

  return new Response(
    JSON.stringify({ embedding: data.data[0].embedding }),
    { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  )
})

Error Handling

Structured Error Response

interface ErrorResponse {
  error: string
  code: string
  details?: unknown
}

function errorResponse(
  message: string,
  code: string,
  status: number,
  details?: unknown
): Response {
  const body: ErrorResponse = { error: message, code, details }
  return new Response(
    JSON.stringify(body),
    { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  )
}

serve(async (req) => {
  try {
    // ... processing
  } catch (error) {
    if (error instanceof AuthError) {
      return errorResponse('Authentication failed', 'AUTH_ERROR', 401)
    }
    if (error instanceof ValidationError) {
      return errorResponse('Invalid input', 'VALIDATION_ERROR', 400, error.details)
    }
    console.error('Unexpected error:', error)
    return errorResponse('Internal server error', 'INTERNAL_ERROR', 500)
  }
})

Deployment

Local Development

supabase functions serve api --env-file .env.local

Deploy Function

supabase functions deploy api

Set Secrets

supabase secrets set OPENAI_API_KEY=sk-xxx
supabase secrets set WEBHOOK_SECRET=whsec-xxx

List Functions

supabase functions list

Best Practices

Cold Start Optimization

Keep imports minimal at the top level:

// Good: Import only what's needed
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

// Bad: Heavy imports at top level increase cold start
// import { everything } from 'large-library'

Response Streaming

Stream large responses:

serve(async (req) => {
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        await new Promise(r => setTimeout(r, 100))
        controller.enqueue(new TextEncoder().encode(`data: ${i}\n\n`))
      }
      controller.close()
    }
  })

  return new Response(stream, {
    headers: { 'Content-Type': 'text/event-stream' }
  })
})

Context7 Query Examples

For latest Edge Functions documentation:

Topic: "edge functions deno runtime" Topic: "supabase functions deploy secrets" Topic: "edge functions cors authentication"


Related Modules:

  • auth-integration.md - Authentication patterns
  • typescript-patterns.md - Client invocation