Edge Functions: Serverless Logic at the Edge

Kevin Dávila
Your frontend asks your backend whether the token is valid. Your backend asks the database. Meanwhile, the user stares at a blank screen. That validation round-trip on every request is time that doesn't need to exist.
Supabase Edge Functions run that logic at the edge, milliseconds away from your users: Deno, TypeScript, and deploys in seconds, with no server to maintain. In this post you'll build your first function and see the patterns where they truly shine: webhooks, custom APIs, and everything you don't want to expose to the client.
What are Edge Functions
Edge Functions are serverless functions that run on Supabase's edge servers (Deno runtime). They're designed for:
Webhooks (Stripe, GitHub, etc.)
Custom APIs
Data processing
Logic that requires server secrets
Integrations with external services
Create your first Edge Function
Initialize Supabase CLI
// terminal
npm install -g supabase
supabase initCreate a function
// terminal
supabase functions new hello-worldThis creates supabase/functions/hello-world/index.ts.
Write the function
// supabase/functions/hello-world/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req) => {
const { name } = await req.json()
const data = {
message: `Hello ${name}!`,
timestamp: new Date().toISOString()
}
return new Response(
JSON.stringify(data),
{ headers: { 'Content-Type': 'application/json' } }
)
})Deploy
// terminal
supabase functions deploy hello-worldCall the function
// src/call-function.ts
import { supabase } from './lib/supabase'
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'Kevin' }
})
console.log(data) // { message: 'Hello Kevin!', timestamp: '...' }Access Supabase inside the function
// supabase/functions/get-tasks/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'
serve(async (req) => {
// Create Supabase client with service role key
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Get tasks (bypass RLS with service role)
const { data, error } = await supabase
.from('tasks')
.select('*')
.limit(10)
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' } }
)
})Authentication in Edge Functions
Verify the user
// supabase/functions/protected/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'
serve(async (req) => {
// Get token from header
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'No authorization header' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
)
// Verify the user
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
}
return new Response(
JSON.stringify({ user }),
{ headers: { 'Content-Type': 'application/json' } }
)
})Call protected function from client
// src/call-protected.ts
const { data, error } = await supabase.functions.invoke('protected', {
headers: {
Authorization: `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`
}
})Stripe Webhook
Real example: receiving Stripe events.
// supabase/functions/stripe-webhook/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'
serve(async (req) => {
const signature = req.headers.get('stripe-signature')
const body = await req.text()
// Verify Stripe signature
// (In production, use Stripe's library to verify)
const event = JSON.parse(body)
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object
await supabase
.from('subscriptions')
.upsert({
user_id: session.metadata.user_id,
stripe_customer_id: session.customer,
status: 'active'
})
break
case 'customer.subscription.deleted':
const subscription = event.data.object
await supabase
.from('subscriptions')
.update({ status: 'cancelled' })
.eq('stripe_customer_id', subscription.customer)
break
}
return new Response(
JSON.stringify({ received: true }),
{ headers: { 'Content-Type': 'application/json' } }
)
})Image processing
// supabase/functions/process-image/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req) => {
const { imageUrl, width, height } = await req.json()
// Download image
const response = await fetch(imageUrl)
const buffer = await response.arrayBuffer()
// Process with an image library (e.g., sharp via WASM)
// ...
return new Response(buffer, {
headers: { 'Content-Type': 'image/webp' }
})
})Environment variables
Edge Functions can access secrets configured in the dashboard:
// Terminal - set secrets
supabase secrets set MY_SECRET=secret_value// Use in function
const secret = Deno.env.get('MY_SECRET')Edge Functions vs Database Functions
Feature | Edge Functions | Database Functions |
|---|---|---|
Runtime | Deno | PostgreSQL/PLpgSQL |
Location | Edge servers | Inside DB |
Latency | Low (edge) | Very low (same DB) |
Use cases | Webhooks, APIs, integrations | Complex business logic |
DB access | Via Supabase client | Direct |
When to use each
Edge Functions:
Need to call external services
Webhooks from Stripe, GitHub, etc.
Logic that depends on server secrets
Custom APIs with complex logic
Database Functions:
Business logic that only touches DB
Stored procedures
Triggers
Operations that need transactions
Common errors
"Function not found"
Verify you deployed the function
Check the name (case-sensitive)
"CORS error"
Add CORS headers in the function:
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// Your logic here
return new Response(data, { headers: { ...corsHeaders, 'Content-Type': 'application/json' } })
})"Timeout"
Edge Functions have a 150 second timeout
For long processes, use background tasks
What's next
In the next article we cover Supabase + AI: how to use pgvector for embeddings, semantic search, and build RAG pipelines directly in your database.