Building with Supabase Edge Functions
HOW TO GUIDES Dec. 30, 2025, 5:30 a.m.

Building with Supabase Edge Functions

Supabase Edge Functions let you run server‑side JavaScript (or TypeScript) right at the edge, close to your users, without managing any infrastructure. In this guide we’ll walk through the core concepts, set up a local development environment, and build three practical functions that solve real‑world problems. By the end you’ll have a solid toolbox for extending Supabase with custom logic that scales automatically.

What are Supabase Edge Functions?

Edge Functions are lightweight, stateless HTTP handlers that execute on Supabase’s global edge network. They sit between your client and Supabase services, giving you a secure place to add business logic, data validation, or third‑party integrations. Because they run on the edge, latency is minimal and you get built‑in scaling—no servers to spin up, no containers to patch.

Under the hood, each function runs in an isolated V8 isolate, similar to Cloudflare Workers. This isolation means you can’t write to a local filesystem, but you can access environment variables, make outbound HTTP calls, and interact with Supabase’s Postgres via the auto‑generated client. The result is a clean, serverless experience that feels like writing a normal Express route, but with far less operational overhead.

Getting Started

Prerequisites

  • A Supabase project (free tier works fine for experimentation).
  • Node.js ≥ 18 installed locally.
  • The Supabase CLI installed globally (npm i -g supabase).
  • Basic familiarity with JavaScript/TypeScript and REST APIs.

Installing the CLI

Open a terminal and run the following command to pull the latest CLI binary. The CLI handles scaffolding, local emulation, and deployment.

npm i -g supabase

After installation, verify the version with supabase --version. You should see a version number ≥ 1.0.0, which includes Edge Function support.

Creating Your First Edge Function

Project scaffolding

Navigate to a directory where you want to keep your functions and run supabase init. This command creates a .supabase folder with a functions subdirectory, a supabase/config.toml file, and a local Docker‑compose stack for emulation.

mkdir my-edge-functions
cd my-edge-functions
supabase init

Inside functions you’ll find a starter hello-world function. Let’s replace it with a more useful example that greets a user by name.

Writing the function

Create a new folder called greet-user and add an index.ts file. The handler receives a request object and must return a JSON response with a proper Content-Type header.

export async function handler(request: Request) {
  const { name } = await request.json()
  const greeting = `👋 Hello, ${name || 'friend'}!`
  return new Response(JSON.stringify({ greeting }), {
    headers: { 'Content-Type': 'application/json' },
  })
}

Notice the use of native Web APIs—no Express needed. The function is automatically bundled by the CLI when you run supabase functions deploy greet-user.

Deploying

First, test locally with supabase functions serve greet-user. The CLI spins up a local edge runtime on http://localhost:54321. Send a POST request using curl or your favorite HTTP client to see the response.

curl -X POST http://localhost:54321/greet-user \\
  -H "Content-Type: application/json" \\
  -d '{"name":"Alex"}'

When you’re satisfied, deploy to the cloud with a single command:

supabase functions deploy greet-user --project-ref YOUR_PROJECT_REF

The function is now live at https://YOUR_PROJECT_ID.functions.supabase.co/greet-user, ready to be called from any client.

Real‑World Example 1: Auth Webhook

Supabase emits auth events (sign‑up, sign‑in, password reset) that you can listen to via Edge Functions. A common pattern is to enrich a newly created user profile with default settings or send a welcome email.

Below is a webhook that runs after a user signs up. It extracts the user ID from the payload, writes a default row into a profiles table, and triggers a SendGrid email.

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)

export async function handler(request: Request) {
  const event = await request.json()
  if (event.type !== 'USER_SIGNUP') {
    return new Response('Ignored', { status: 200 })
  }

  const userId = event.data.id
  const { error } = await supabase
    .from('profiles')
    .insert({ id: userId, theme: 'light', notifications: true })

  if (error) {
    console.error('Profile insert failed:', error)
    return new Response('Error', { status: 500 })
  }

  // Send welcome email via SendGrid
  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: event.data.email }] }],
      from: { email: 'welcome@myapp.com' },
      subject: 'Welcome to MyApp!',
      content: [{ type: 'text/plain', value: 'Thanks for joining us.' }],
    }),
  })

  return new Response('User onboarded', { status: 200 })
}

This function demonstrates three important capabilities: reading the event payload, using the Supabase client with a service role key (so you can bypass RLS), and calling an external API. Because it runs at the edge, the user experiences virtually no delay between sign‑up and receiving the welcome email.

Pro tip: Store secret keys (e.g., SENDGRID_API_KEY) in Supabase’s Project Settings → API → Service Role section and reference them via Deno.env.get(). Never hard‑code secrets in your source files.

Real‑World Example 2: Image Resizer

Suppose your app lets users upload avatars. You want to store a thumbnail version alongside the original to improve UI performance. An Edge Function can fetch the uploaded image from Supabase Storage, resize it with sharp, and write the thumbnail back—all without a dedicated backend server.

First, add sharp as a dependency in functions/image-resizer/package.json. Then write the handler:

import { createClient } from '@supabase/supabase-js'
import sharp from 'sharp'

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)

export async function handler(request: Request) {
  const { bucket, path } = await request.json()
  const { data, error } = await supabase.storage.from(bucket).download(path)

  if (error) {
    console.error('Download failed:', error)
    return new Response('File not found', { status: 404 })
  }

  const buffer = await data.arrayBuffer()
  const thumbnail = await sharp(Buffer.from(buffer))
    .resize(150, 150)
    .png()
    .toBuffer()

  const thumbPath = path.replace(/(\.[^.]+)$/, '_thumb$1')
  const { error: uploadError } = await supabase.storage
    .from(bucket)
    .upload(thumbPath, thumbnail, { contentType: 'image/png' })

  if (uploadError) {
    console.error('Upload failed:', uploadError)
    return new Response('Upload error', { status: 500 })
  }

  return new Response(JSON.stringify({ thumbnail_path: thumbPath }), {
    headers: { 'Content-Type': 'application/json' },
  })
}

Trigger this function from a client after a successful upload, or set up a Supabase Storage webhook that automatically calls the function whenever a new file lands in the bucket. The result is an instantly available thumbnail that you can serve directly from the CDN.

Pro tip: Keep your thumbnail dimensions consistent (e.g., 150×150) to benefit from browser caching. Also, enable Cache-Control headers on the uploaded thumbnail to reduce repeat fetches.

Real‑World Example 3: Scheduled Cleanup

Edge Functions support cron‑style schedules via Supabase’s built‑in scheduler. A practical use case is purging stale rows from a temp_sessions table every night, freeing up space and keeping analytics clean.

First, create a function that runs a simple DELETE query. Because the function runs in a trusted environment, you can use the service role key to bypass row‑level security.

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)

export async function handler() {
  const { error } = await supabase
    .rpc('cleanup_temp_sessions') // assumes a Postgres function exists
  if (error) {
    console.error('Cleanup failed:', error)
    return new Response('Error', { status: 500 })
  }
  return new Response('Cleanup complete', { status: 200 })
}

Next, register the schedule with the CLI:

supabase functions schedule cleanup-sessions \\
  --cron "0 2 * * *" \\
  --function-name cleanup-temp-sessions

The function will now run at 02:00 UTC every day. You can monitor its executions in the Supabase dashboard under “Functions → Logs”.

Pro tip: Wrap complex cleanup logic inside a Postgres stored procedure (as shown with rpc('cleanup_temp_sessions')) to keep the Edge Function thin and fast. This also allows you to reuse the same cleanup logic from other tools.

Best Practices & Performance Tips

  • Keep functions stateless. Rely on Supabase storage, Postgres, or external services for persistence.
  • Leverage the service role key sparingly. Only grant it to functions that truly need elevated privileges.
  • Minimize bundle size. Import only the modules you use; unused dependencies increase cold‑start latency.
  • Use native Web APIs. Functions run in a V8 isolate, so fetch, Response, and Headers are available without extra libraries.
  • Set appropriate cache headers. For static assets (e.g., thumbnails), return Cache-Control: public, max-age=86400 to let CDNs serve them for a day.
  • Monitor logs. The CLI offers supabase functions logs <name> for real‑time debugging.
Pro tip: Enable “Edge Function logs” retention in your Supabase project settings. Longer retention periods help you investigate intermittent bugs that only appear after weeks of production traffic.

Testing and Debugging

During development, run supabase functions serve <name> to spin up a local edge runtime. It mirrors the production environment, including the same request/response objects. Use tools like curl, Postman, or VS Code’s REST client to send test payloads.

If a function throws an error, the CLI prints a stack trace with line numbers. Remember that the V8 isolate strips away Node.js‑specific globals, so process.env is unavailable—use Deno.env.get() instead.

For unit testing, you can import the handler directly into a Jest or Vitest suite. Mock the Supabase client with @supabase/supabase-js’s createClient and replace network calls with stubs.

Monitoring and Observability

Supabase automatically captures request latency, error rates, and invocation counts. In the dashboard, navigate to “Functions → Metrics” to see a time‑series chart of each function’s performance. Set up alerts on error thresholds to get notified via Slack or email.

For deeper insight, instrument your code with OpenTelemetry. Export traces to a third‑party observability platform (e.g., Datadog) by sending them over HTTP from within the function. Because Edge Functions run in a constrained environment, keep telemetry payloads small to avoid exceeding the 5 MB response limit.

Conclusion

Supabase Edge Functions give you the power of serverless compute right at the network edge, without the operational baggage of traditional backends. By following the patterns outlined above—auth webhooks, media processing, and scheduled tasks—you can extend your Supabase applications with custom logic that is fast, secure, and automatically scalable. Remember to keep functions stateless, protect your secrets, and monitor performance closely. With these tools in your arsenal, building robust, real‑time applications on Supabase becomes not just possible, but delightfully straightforward.

Share this article