Codea Bien Logo
Edge Functions: Serverless Logic at the Edge
Supabase

Edge Functions: Serverless Logic at the Edge

Kevin Dávila

Kevin Dávila

supabase edge functionsserverlessdenowebhookssupabase functions

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 init

Create a function

// terminal
supabase functions new hello-world

This 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-world

Call 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.