Supabase Edge Functions: Serverless Backend Logic

Supabase Edge Functions provide globally distributed, serverless TypeScript/JavaScript functions running on Deno Deploy, enabling custom server-side logic for webhooks, scheduled tasks, third-party API integrations, complex business logic, email notifications, payment processing, and operations requiring server-side execution or secret keys. Unlike client-side code that exposes API keys and can be manipulated, Edge Functions execute securely on Supabase infrastructure with access to environment variables, service role keys, and external APIs while maintaining low latency through global edge deployment. This comprehensive guide covers creating Edge Functions locally, deploying to production, handling HTTP requests and responses, integrating with Supabase services (database, auth, storage), calling external APIs, implementing webhooks (Stripe, SendGrid), scheduling functions with cron jobs, environment variables and secrets management, CORS configuration, and debugging techniques. Edge Functions complement Supabase's client-side capabilities by handling sensitive operations, complex calculations, and integrations that must remain server-side. Before proceeding, understand Supabase architecture and complete project setup.
What Are Edge Functions?
| Use Case | Description | Example | Why Edge Function? |
|---|---|---|---|
| Webhooks | Receive third-party events | Stripe payments, SendGrid | Secret validation |
| Email Sending | Send transactional emails | Welcome emails, notifications | API keys hidden |
| API Integration | Call external services | OpenAI, Twilio, payment gateways | Server-side only |
| Data Processing | Complex transformations | Image processing, PDF generation | Heavy computation |
| Scheduled Tasks | Cron jobs | Daily reports, cleanup | Background processing |
Setting Up Edge Functions
# Install Supabase CLI
npm install -g supabase
# Login to Supabase
supabase login
# Initialize project (if not already done)
supabase init
# Create new Edge Function
supabase functions new hello-world
# This creates:
# supabase/
# functions/
# hello-world/
# index.ts
# Serve locally for development
supabase functions serve
# Serve specific function
supabase functions serve hello-world --env-file ./supabase/.env.local
# Deploy to production
supabase functions deploy hello-world
# Deploy all functions
supabase functions deployCreating Your First Function
// supabase/functions/hello-world/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
serve(async (req) => {
// Handle CORS for browser requests
if (req.method === 'OPTIONS') {
return new Response('ok', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
}
})
}
try {
// Get request data
const { name } = await req.json()
// Process and return response
const data = {
message: `Hello ${name || 'World'}!`,
timestamp: new Date().toISOString()
}
return new Response(
JSON.stringify(data),
{
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
status: 200
}
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
headers: { 'Content-Type': 'application/json' },
status: 400
}
)
}
})
// Call from client:
// const { data, error } = await supabase.functions.invoke('hello-world', {
// body: { name: 'John' }
// })Accessing Supabase Database
// supabase/functions/get-user-stats/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
try {
// Create Supabase client with service role (bypasses RLS)
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Get user from auth header
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const { data: { user } } = await supabaseClient.auth.getUser(token)
if (!user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401 }
)
}
// Query database
const { data: posts, error } = await supabaseClient
.from('posts')
.select('*')
.eq('user_id', user.id)
if (error) throw error
const stats = {
user_id: user.id,
total_posts: posts.length,
posts: posts
}
return new Response(
JSON.stringify(stats),
{ headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500 }
)
}
})Calling External APIs
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
serve(async (req) => {
try {
const { to, subject, text } = await req.json()
// Call SendGrid API (or any email service)
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('SENDGRID_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
personalizations: [{ to: [{ email: to }] }],
from: { email: '[email protected]' },
subject: subject,
content: [{ type: 'text/plain', value: text }]
})
})
if (!response.ok) {
throw new Error('Failed to send email')
}
return new Response(
JSON.stringify({ success: true }),
{ headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500 }
)
}
})
// Environment variables in supabase/.env.local:
// SENDGRID_API_KEY=your-api-keyHandling Webhooks (Stripe Example)
// supabase/functions/stripe-webhook/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import Stripe from 'https://esm.sh/[email protected]'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') || '', {
apiVersion: '2023-10-16'
})
serve(async (req) => {
const signature = req.headers.get('stripe-signature')
const body = await req.text()
try {
// Verify webhook signature
const event = stripe.webhooks.constructEvent(
body,
signature!,
Deno.env.get('STRIPE_WEBHOOK_SECRET') || ''
)
// Create Supabase client
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Handle different event types
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object
// Update user's subscription in database
await supabaseClient
.from('subscriptions')
.insert({
user_id: session.metadata.user_id,
stripe_customer_id: session.customer,
status: 'active'
})
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object
// Mark subscription as canceled
await supabaseClient
.from('subscriptions')
.update({ status: 'canceled' })
.eq('stripe_customer_id', subscription.customer)
break
}
}
return new Response(
JSON.stringify({ received: true }),
{ status: 200 }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400 }
)
}
})Calling Edge Functions from Client
// Call Edge Function from JavaScript client
import { supabase } from './supabaseClient'
// Basic invocation
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'John' }
})
if (error) {
console.error('Function error:', error)
} else {
console.log('Response:', data)
}
// With custom headers
const { data, error } = await supabase.functions.invoke('send-email', {
body: {
to: '[email protected]',
subject: 'Welcome!',
text: 'Thanks for signing up!'
},
headers: {
'X-Custom-Header': 'value'
}
})
// GET request
const { data, error } = await supabase.functions.invoke('get-user-stats')
// Call function URL directly
const response = await fetch(
'https://your-project.supabase.co/functions/v1/hello-world',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${supabase.auth.session()?.access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'John' })
}
)
const data = await response.json()Environment Variables
# Create supabase/.env.local for local development
SUPABASE_URL=your-project-url
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
SENDGRID_API_KEY=your-sendgrid-key
STRIPE_SECRET_KEY=your-stripe-key
STRIPE_WEBHOOK_SECRET=your-webhook-secret
# Access in Edge Function
const apiKey = Deno.env.get('SENDGRID_API_KEY')
# Set secrets for production (using Supabase CLI)
supabase secrets set SENDGRID_API_KEY=your-key
supabase secrets set STRIPE_SECRET_KEY=your-key
# List secrets
supabase secrets list
# Unset secret
supabase secrets unset SENDGRID_API_KEYEdge Functions Best Practices
- Handle CORS Properly: Return appropriate CORS headers for browser requests including OPTIONS preflight
- Validate Input: Always validate request data and handle errors gracefully
- Use Service Role Carefully: Service role bypasses RLSβonly use when necessary and validate permissions
- Keep Functions Small: Each function should do one thing well for easier debugging and maintenance
- Secure Secrets: Never commit API keys to Git; use environment variables and Supabase secrets
- Test Locally First: Use supabase functions serve to test before deploying to production
- Monitor Performance: Edge Functions have execution time limits (25 seconds default)
Common Issues
- CORS Errors: Ensure you handle OPTIONS requests and return proper Access-Control headers
- Environment Variables Not Working: Check .env.local exists and use --env-file flag when serving locally
- Function Timeout: Functions have 25s execution limit by default; optimize long-running tasks
- Import Errors: Use full URLs for imports (https://deno.land/... or https://esm.sh/...)
Next Steps
- Build Complete Apps: Integrate Edge Functions with React applications or Next.js projects
- Learn Migrations: Manage schema changes with database migrations
- Implement Webhooks: Set up Stripe, SendGrid, or other third-party webhooks
- Schedule Tasks: Create cron jobs for background processing and scheduled operations
Conclusion
Supabase Edge Functions provide serverless TypeScript/JavaScript execution on globally distributed infrastructure, enabling custom backend logic that must remain server-side for security or complexity reasons. By running on Deno with access to Supabase services and external APIs, Edge Functions complement your client-side application with webhooks, email sending, payment processing, complex transformations, and scheduled tasks. The combination of edge deployment for low latency, environment variables for secrets management, and service role access for administrative operations makes Edge Functions suitable for production workloads. Always remember to handle CORS properly, validate input thoroughly, secure API keys with environment variables, and test locally before deploying. With Edge Functions mastered, you have complete control over your backend logic while maintaining Supabase's developer-friendly approach. Continue building production applications with complete tutorials and framework integrations.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


