372 lines
8.2 KiB
Markdown
372 lines
8.2 KiB
Markdown
---
|
|
name: edge-functions
|
|
description: Serverless Deno functions at the edge with authentication and rate limiting
|
|
parent-skill: moai-platform-supabase
|
|
version: 1.0.0
|
|
updated: 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```bash
|
|
supabase functions serve api --env-file .env.local
|
|
```
|
|
|
|
### Deploy Function
|
|
|
|
```bash
|
|
supabase functions deploy api
|
|
```
|
|
|
|
### Set Secrets
|
|
|
|
```bash
|
|
supabase secrets set OPENAI_API_KEY=sk-xxx
|
|
supabase secrets set WEBHOOK_SECRET=whsec-xxx
|
|
```
|
|
|
|
### List Functions
|
|
|
|
```bash
|
|
supabase functions list
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Cold Start Optimization
|
|
|
|
Keep imports minimal at the top level:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
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
|