Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Use Supabase Edge Functions

Supabase Edge Functions are server-side TypeScript functions running on a Deno runtime at the edge. You create them with the Supabase CLI using supabase functions new, write your logic in index.ts using the Deno.serve pattern, test locally with supabase functions serve, and deploy with supabase functions deploy. They are ideal for webhooks, third-party API calls, and any logic that should not run in the browser.

What you'll learn

  • How to create and structure an Edge Function with the Supabase CLI
  • How to serve Edge Functions locally for development and testing
  • How to deploy Edge Functions and manage secrets in production
  • How to handle CORS and authenticate requests inside Edge Functions
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced8 min read15-20 minSupabase (all plans), Supabase CLI v1.50+, Deno runtimeMarch 2026RapidDev Engineering Team
TL;DR

Supabase Edge Functions are server-side TypeScript functions running on a Deno runtime at the edge. You create them with the Supabase CLI using supabase functions new, write your logic in index.ts using the Deno.serve pattern, test locally with supabase functions serve, and deploy with supabase functions deploy. They are ideal for webhooks, third-party API calls, and any logic that should not run in the browser.

Creating, Testing, and Deploying Supabase Edge Functions

Supabase Edge Functions let you run server-side TypeScript code on a globally distributed Deno runtime. They are perfect for tasks that must not happen in the browser — calling third-party APIs with secret keys, processing webhooks, running server-side validation, or orchestrating multi-step backend logic. This tutorial walks you through the full lifecycle: scaffolding a function, writing your handler, testing locally, deploying to production, and managing secrets.

Prerequisites

  • A Supabase project (free tier or above)
  • Supabase CLI installed (brew install supabase/tap/supabase or npm install supabase --save-dev)
  • Node.js 18+ and a code editor
  • Basic familiarity with TypeScript and HTTP concepts

Step-by-step guide

1

Initialize your project and create a new Edge Function

If you have not already initialized Supabase in your project, run supabase init to create the supabase/ directory structure. Then scaffold a new Edge Function. This creates a folder at supabase/functions/hello-world/ with an index.ts file containing a starter template. Each function lives in its own folder under supabase/functions/.

typescript
1# Initialize Supabase (skip if already done)
2supabase init
3
4# Create a new Edge Function
5supabase functions new hello-world
6
7# Resulting structure:
8# supabase/
9# functions/
10# hello-world/
11# index.ts

Expected result: A new folder supabase/functions/hello-world/ with an index.ts file is created.

2

Write the Edge Function handler with CORS support

Open supabase/functions/hello-world/index.ts and replace the contents. Every Edge Function uses the Deno.serve() pattern. You must handle CORS manually because Edge Functions do not inherit the REST API's CORS configuration. Create a shared CORS headers file and import it. The OPTIONS preflight handler is critical — without it, browser requests will fail silently.

typescript
1// supabase/functions/_shared/cors.ts
2export const corsHeaders = {
3 'Access-Control-Allow-Origin': '*',
4 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
5 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
6}
7
8// supabase/functions/hello-world/index.ts
9import { corsHeaders } from '../_shared/cors.ts'
10
11Deno.serve(async (req) => {
12 // Handle CORS preflight
13 if (req.method === 'OPTIONS') {
14 return new Response('ok', { headers: corsHeaders })
15 }
16
17 const { name } = await req.json()
18 const data = { message: `Hello ${name}!` }
19
20 return new Response(JSON.stringify(data), {
21 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
22 status: 200,
23 })
24})

Expected result: The Edge Function file is saved with proper CORS handling and a JSON response.

3

Access Supabase services inside the function

Edge Functions have automatic access to SUPABASE_URL, SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY as environment variables. You can create a Supabase client inside the function to query your database, manage auth, or access storage. Use the anon key for user-context operations (respects RLS) and the service role key for admin operations (bypasses RLS — never expose this key to the client).

typescript
1import { createClient } from 'npm:@supabase/supabase-js@2'
2import { corsHeaders } from '../_shared/cors.ts'
3
4Deno.serve(async (req) => {
5 if (req.method === 'OPTIONS') {
6 return new Response('ok', { headers: corsHeaders })
7 }
8
9 // Create client with user's auth context
10 const supabase = createClient(
11 Deno.env.get('SUPABASE_URL')!,
12 Deno.env.get('SUPABASE_ANON_KEY')!,
13 {
14 global: {
15 headers: { Authorization: req.headers.get('Authorization')! },
16 },
17 }
18 )
19
20 const { data, error } = await supabase.from('todos').select('*')
21
22 return new Response(JSON.stringify({ data, error }), {
23 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
24 })
25})

Expected result: The Edge Function can query your Supabase database while respecting Row Level Security policies.

4

Test locally with supabase functions serve

Run supabase functions serve to start a local development server with hot reload. This serves all your Edge Functions at http://localhost:54321/functions/v1/<function-name>. For local secrets, create a supabase/functions/.env file with any custom environment variables. The default Supabase keys (URL, anon key, service role key) are automatically injected from your local Supabase instance.

typescript
1# Start local Supabase (if not running)
2supabase start
3
4# Serve Edge Functions with hot reload
5supabase functions serve
6
7# Test with curl
8curl -i --location --request POST \
9 'http://localhost:54321/functions/v1/hello-world' \
10 --header 'Authorization: Bearer YOUR_ANON_KEY' \
11 --header 'Content-Type: application/json' \
12 --data '{"name": "World"}'

Expected result: The function responds with {"message": "Hello World!"} on localhost.

5

Set production secrets and deploy

Before deploying, set any custom secrets your function needs (third-party API keys, tokens, etc.). The default Supabase secrets (URL, keys) are already available in production. Then deploy your function. You can deploy a single function or all functions at once. After deployment, the function is available at https://<project-ref>.supabase.co/functions/v1/<function-name>.

typescript
1# Link to your remote project
2supabase link --project-ref your-project-ref
3
4# Set custom secrets
5supabase secrets set STRIPE_SECRET_KEY=sk_live_...
6supabase secrets set OPENAI_API_KEY=sk-...
7
8# Deploy a single function
9supabase functions deploy hello-world
10
11# Deploy all functions at once
12supabase functions deploy

Expected result: The function is deployed and accessible at your project's Edge Function URL.

6

Invoke the deployed function from your frontend

Use the Supabase JS client's functions.invoke() method to call your Edge Function from the browser. The client automatically includes the user's auth token and the project's anon key. This is the recommended way to call Edge Functions because it handles authentication headers for you.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 'https://your-project.supabase.co',
5 'your-anon-key'
6)
7
8const { data, error } = await supabase.functions.invoke('hello-world', {
9 body: { name: 'World' },
10})
11
12console.log(data) // { message: 'Hello World!' }

Expected result: The frontend receives the JSON response from the Edge Function.

Complete working example

supabase/functions/hello-world/index.ts
1// supabase/functions/hello-world/index.ts
2// A complete Edge Function with CORS, auth, and database access
3
4import { createClient } from 'npm:@supabase/supabase-js@2'
5
6const corsHeaders = {
7 'Access-Control-Allow-Origin': '*',
8 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
9 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
10}
11
12Deno.serve(async (req) => {
13 // Handle CORS preflight requests
14 if (req.method === 'OPTIONS') {
15 return new Response('ok', { headers: corsHeaders })
16 }
17
18 try {
19 // Parse the request body
20 const { name } = await req.json()
21
22 if (!name) {
23 return new Response(
24 JSON.stringify({ error: 'Name is required' }),
25 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
26 )
27 }
28
29 // Create a Supabase client with the user's auth context
30 const supabase = createClient(
31 Deno.env.get('SUPABASE_URL')!,
32 Deno.env.get('SUPABASE_ANON_KEY')!,
33 {
34 global: {
35 headers: { Authorization: req.headers.get('Authorization')! },
36 },
37 }
38 )
39
40 // Verify the user is authenticated
41 const { data: { user }, error: authError } = await supabase.auth.getUser()
42 if (authError || !user) {
43 return new Response(
44 JSON.stringify({ error: 'Unauthorized' }),
45 { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
46 )
47 }
48
49 // Perform a database operation (RLS applies)
50 const { data, error } = await supabase
51 .from('greetings')
52 .insert({ user_id: user.id, name, greeted_at: new Date().toISOString() })
53 .select()
54 .single()
55
56 if (error) {
57 return new Response(
58 JSON.stringify({ error: error.message }),
59 { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
60 )
61 }
62
63 return new Response(
64 JSON.stringify({ message: `Hello ${name}!`, record: data }),
65 { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
66 )
67 } catch (err) {
68 return new Response(
69 JSON.stringify({ error: 'Internal server error' }),
70 { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
71 )
72 }
73})

Common mistakes when using Supabase Edge Functions

Why it's a problem: Forgetting to handle the OPTIONS preflight request, causing CORS errors in the browser

How to avoid: Always add an OPTIONS handler at the top of your function that returns a 200 with CORS headers. Without it, browsers will block cross-origin requests.

Why it's a problem: Using SUPABASE_SERVICE_ROLE_KEY for all operations instead of passing the user's Authorization header

How to avoid: Pass the user's Authorization header to the Supabase client so RLS policies evaluate correctly. Only use the service role key for admin operations that intentionally bypass RLS.

Why it's a problem: Not including CORS headers in error responses, making it impossible to read error messages in the browser

How to avoid: Include corsHeaders in every response, including 400, 401, and 500 responses. Otherwise the browser will show a generic CORS error.

Why it's a problem: Trying to import npm packages without the npm: prefix in the Deno runtime

How to avoid: Edge Functions use Deno, not Node.js. Import npm packages with the npm: prefix: import { createClient } from 'npm:@supabase/supabase-js@2'.

Best practices

  • Create a shared _shared/cors.ts file and import it in every Edge Function to keep CORS handling consistent
  • Always validate and sanitize request input before processing — check for required fields and expected types
  • Use the user's Authorization header with the anon key for user-context operations so RLS policies apply correctly
  • Store all third-party API keys as secrets with supabase secrets set, never hardcode them in function code
  • Return structured JSON error responses with appropriate HTTP status codes for easier debugging
  • Keep Edge Functions focused on a single task — split complex logic across multiple functions
  • Test locally with supabase functions serve before deploying to catch import and runtime errors early
  • Monitor function logs in the Supabase Dashboard under Edge Functions > Logs to debug production issues

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to create a Supabase Edge Function that receives a POST request with a user message, calls the OpenAI API with a secret key, and returns the AI response. Show me the complete index.ts file with CORS handling, input validation, error handling, and how to set the OPENAI_API_KEY as a secret.

Supabase Prompt

Create a Supabase Edge Function at supabase/functions/process-order/index.ts that receives an order payload, validates the user is authenticated, inserts the order into an orders table respecting RLS, and returns the created order. Include proper CORS headers and error handling.

Frequently asked questions

What runtime do Supabase Edge Functions use?

Supabase Edge Functions run on a Deno-compatible runtime distributed globally at the edge. They use TypeScript natively and support npm packages via the npm: import prefix.

What are the limits for Edge Functions?

Edge Functions have a 150-second wall-clock timeout for the initial response, 2 seconds of CPU time per request, a 20 MB bundle size limit, and approximately 150 MB of memory. Pro plans allow background tasks up to 400 seconds after the initial response.

Do I need to redeploy after changing secrets?

No. Secrets take effect on the next function invocation without redeployment. Use supabase secrets set KEY=VALUE to update them.

Can I use npm packages in Edge Functions?

Yes. Import npm packages using the npm: prefix, for example: import { createClient } from 'npm:@supabase/supabase-js@2'. Deno handles the resolution automatically.

How do I debug Edge Function errors in production?

Check the Edge Functions logs in the Supabase Dashboard under Edge Functions > Logs. Use console.log() statements in your function code — they appear in the logs. Common errors include BOOT_ERROR (import failures) and TIME_LIMIT (exceeded timeout).

Can I disable JWT verification for public webhooks?

Yes. Deploy with the --no-verify-jwt flag: supabase functions deploy my-webhook --no-verify-jwt. This allows unauthenticated requests, which is necessary for receiving webhooks from external services.

Can RapidDev help build and deploy Edge Functions for my project?

Yes. RapidDev can architect, build, and deploy Edge Functions for complex use cases like webhook processing, third-party API orchestration, and server-side validation logic.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.