mirror of
https://github.com/greedychipmunk/agent-skills.git
synced 2026-09-14 20:46:42 +08:00
14 KiB
14 KiB
Edge Functions
Overview
Supabase Edge Functions are server-side TypeScript/JavaScript functions that run on Deno Deploy. They enable:
- Custom API endpoints
- Webhook handlers
- Third-party integrations
- Scheduled tasks
- Background processing
Project Structure
supabase/
├── functions/
│ ├── hello-world/
│ │ └── index.ts
│ ├── send-email/
│ │ └── index.ts
│ ├── webhook-handler/
│ │ └── index.ts
│ └── _shared/
│ ├── cors.ts
│ ├── supabase-client.ts
│ └── validation.ts
└── config.toml
Basic Function Structure
// supabase/functions/hello-world/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req: Request) => {
const { name } = await req.json()
return new Response(
JSON.stringify({ message: `Hello, ${name}!` }),
{
headers: { 'Content-Type': 'application/json' },
status: 200
}
)
})
CORS Handling
Shared CORS Module
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}
export function handleCors(req: Request) {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
return null
}
Using CORS
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { corsHeaders, handleCors } from '../_shared/cors.ts'
serve(async (req) => {
// Handle CORS preflight
const corsResponse = handleCors(req)
if (corsResponse) return corsResponse
try {
// Your logic here
return new Response(
JSON.stringify({ success: true }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
})
Supabase Client in Functions
Service Role Client
// supabase/functions/_shared/supabase-client.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
export function createServiceClient() {
return createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
}
export function createUserClient(authHeader: string) {
return createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{
global: {
headers: { Authorization: authHeader }
}
}
)
}
Using Authenticated User Context
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createUserClient } from '../_shared/supabase-client.ts'
import { corsHeaders, handleCors } from '../_shared/cors.ts'
serve(async (req) => {
const corsResponse = handleCors(req)
if (corsResponse) return corsResponse
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Missing authorization header' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
const supabase = createUserClient(authHeader)
// Get the authenticated user
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
return new Response(
JSON.stringify({ error: 'Invalid token' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// User is authenticated, proceed with logic
const { data, error } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id)
return new Response(
JSON.stringify({ posts: data }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
})
Request Handling
HTTP Methods
serve(async (req) => {
const { method, url } = req
const path = new URL(url).pathname
switch (method) {
case 'GET':
return handleGet(req)
case 'POST':
return handlePost(req)
case 'PUT':
return handlePut(req)
case 'DELETE':
return handleDelete(req)
default:
return new Response('Method not allowed', { status: 405 })
}
})
Query Parameters
serve(async (req) => {
const url = new URL(req.url)
const page = parseInt(url.searchParams.get('page') || '1')
const limit = parseInt(url.searchParams.get('limit') || '10')
const search = url.searchParams.get('search')
// Use parameters...
})
Request Body
serve(async (req) => {
// JSON body
const body = await req.json()
// Form data
const formData = await req.formData()
const file = formData.get('file') as File
// Text body
const text = await req.text()
// Raw bytes
const bytes = await req.arrayBuffer()
})
Input Validation
// Using Zod for validation
import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts'
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
published: z.boolean().default(false)
})
serve(async (req) => {
const corsResponse = handleCors(req)
if (corsResponse) return corsResponse
try {
const body = await req.json()
const data = CreatePostSchema.parse(body)
// data is now typed and validated
return new Response(
JSON.stringify({ success: true, data }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(
JSON.stringify({ error: 'Validation failed', details: error.errors }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
throw error
}
})
Webhook Handling
Stripe Webhook
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import Stripe from 'https://esm.sh/stripe@14.0.0?target=deno'
import { createServiceClient } from '../_shared/supabase-client.ts'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
apiVersion: '2023-10-16',
httpClient: Stripe.createFetchHttpClient()
})
const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET')!
serve(async (req) => {
const signature = req.headers.get('stripe-signature')!
const body = await req.text()
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 })
}
const supabase = createServiceClient()
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
await supabase
.from('orders')
.update({ status: 'paid' })
.eq('stripe_session_id', session.id)
break
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription
await supabase
.from('subscriptions')
.update({
status: subscription.status,
current_period_end: new Date(subscription.current_period_end * 1000)
})
.eq('stripe_subscription_id', subscription.id)
break
}
}
return new Response(JSON.stringify({ received: true }), { status: 200 })
})
GitHub Webhook
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createServiceClient } from '../_shared/supabase-client.ts'
serve(async (req) => {
const event = req.headers.get('x-github-event')
const payload = await req.json()
const supabase = createServiceClient()
switch (event) {
case 'push':
await supabase.from('deployments').insert({
repo: payload.repository.full_name,
commit: payload.head_commit.id,
message: payload.head_commit.message,
pusher: payload.pusher.name
})
break
case 'issues':
if (payload.action === 'opened') {
// Handle new issue
}
break
}
return new Response('OK', { status: 200 })
})
Third-Party API Integration
Email with Resend
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { corsHeaders, handleCors } from '../_shared/cors.ts'
serve(async (req) => {
const corsResponse = handleCors(req)
if (corsResponse) return corsResponse
const { to, subject, html } = await req.json()
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'noreply@yourapp.com',
to,
subject,
html
})
})
const data = await response.json()
return new Response(
JSON.stringify(data),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
})
OpenAI Integration
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { corsHeaders, handleCors } from '../_shared/cors.ts'
serve(async (req) => {
const corsResponse = handleCors(req)
if (corsResponse) return corsResponse
const { prompt } = await req.json()
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
max_tokens: 1000
})
})
const data = await response.json()
return new Response(
JSON.stringify({
response: data.choices[0].message.content
}),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
})
Scheduled Tasks (pg_cron)
Setup pg_cron
-- Enable extension
create extension if not exists pg_cron;
-- Grant access
grant usage on schema cron to postgres;
grant all privileges on all tables in schema cron to postgres;
Schedule Function Calls
-- Call Edge Function daily at midnight
select cron.schedule(
'daily-cleanup',
'0 0 * * *',
$$
select net.http_post(
url := 'https://your-project.supabase.co/functions/v1/cleanup',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || current_setting('app.service_role_key'),
'Content-Type', 'application/json'
),
body := '{}'::jsonb
);
$$
);
-- List scheduled jobs
select * from cron.job;
-- Remove job
select cron.unschedule('daily-cleanup');
Local Development
Start Functions Locally
# Start all functions
supabase functions serve
# Start specific function
supabase functions serve hello-world
# With environment variables
supabase functions serve --env-file .env.local
Testing Locally
# cURL
curl -i --location --request POST \
'http://localhost:54321/functions/v1/hello-world' \
--header 'Authorization: Bearer YOUR_ANON_KEY' \
--header 'Content-Type: application/json' \
--data '{"name": "World"}'
# Using Supabase client
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'World' }
})
Deployment
Deploy Functions
# Deploy all functions
supabase functions deploy
# Deploy specific function
supabase functions deploy hello-world
# Deploy with environment variables
supabase secrets set RESEND_API_KEY=re_xxx
# List secrets
supabase secrets list
Environment Variables
# Set secrets
supabase secrets set API_KEY=xxx SECRET_KEY=yyy
# Unset secrets
supabase secrets unset API_KEY
# Available by default:
# - SUPABASE_URL
# - SUPABASE_ANON_KEY
# - SUPABASE_SERVICE_ROLE_KEY
# - SUPABASE_DB_URL
Best Practices
Error Handling
serve(async (req) => {
try {
// Your logic
} catch (error) {
console.error('Function error:', error)
// Don't expose internal errors
const isProduction = Deno.env.get('ENVIRONMENT') === 'production'
return new Response(
JSON.stringify({
error: isProduction ? 'Internal server error' : error.message
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
})
Logging
// Logs are visible in Supabase Dashboard
console.log('Processing request:', req.url)
console.info('User action:', { userId, action })
console.warn('Deprecated API used')
console.error('Failed to process:', error)
Response Helpers
// supabase/functions/_shared/response.ts
import { corsHeaders } from './cors.ts'
export function json(data: unknown, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
export function error(message: string, status = 400) {
return json({ error: message }, status)
}
export function unauthorized(message = 'Unauthorized') {
return error(message, 401)
}
export function notFound(message = 'Not found') {
return error(message, 404)
}
Rate Limiting
const rateLimits = new Map<string, { count: number; resetAt: number }>()
function checkRateLimit(ip: string, limit = 100, windowMs = 60000): boolean {
const now = Date.now()
const record = rateLimits.get(ip)
if (!record || record.resetAt < now) {
rateLimits.set(ip, { count: 1, resetAt: now + windowMs })
return true
}
if (record.count >= limit) {
return false
}
record.count++
return true
}
serve(async (req) => {
const ip = req.headers.get('x-forwarded-for') || 'unknown'
if (!checkRateLimit(ip)) {
return new Response('Rate limit exceeded', { status: 429 })
}
// Continue with request
})