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
Initialize your project and create a new Edge Function
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/.
1# Initialize Supabase (skip if already done)2supabase init34# Create a new Edge Function5supabase functions new hello-world67# Resulting structure:8# supabase/9# functions/10# hello-world/11# index.tsExpected result: A new folder supabase/functions/hello-world/ with an index.ts file is created.
Write the Edge Function handler with CORS support
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.
1// supabase/functions/_shared/cors.ts2export 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}78// supabase/functions/hello-world/index.ts9import { corsHeaders } from '../_shared/cors.ts'1011Deno.serve(async (req) => {12 // Handle CORS preflight13 if (req.method === 'OPTIONS') {14 return new Response('ok', { headers: corsHeaders })15 }1617 const { name } = await req.json()18 const data = { message: `Hello ${name}!` }1920 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.
Access Supabase services inside the function
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).
1import { createClient } from 'npm:@supabase/supabase-js@2'2import { corsHeaders } from '../_shared/cors.ts'34Deno.serve(async (req) => {5 if (req.method === 'OPTIONS') {6 return new Response('ok', { headers: corsHeaders })7 }89 // Create client with user's auth context10 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 )1920 const { data, error } = await supabase.from('todos').select('*')2122 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.
Test locally with supabase functions serve
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.
1# Start local Supabase (if not running)2supabase start34# Serve Edge Functions with hot reload5supabase functions serve67# Test with curl8curl -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.
Set production secrets and deploy
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>.
1# Link to your remote project2supabase link --project-ref your-project-ref34# Set custom secrets5supabase secrets set STRIPE_SECRET_KEY=sk_live_...6supabase secrets set OPENAI_API_KEY=sk-...78# Deploy a single function9supabase functions deploy hello-world1011# Deploy all functions at once12supabase functions deployExpected result: The function is deployed and accessible at your project's Edge Function URL.
Invoke the deployed function from your frontend
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.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 'https://your-project.supabase.co',5 'your-anon-key'6)78const { data, error } = await supabase.functions.invoke('hello-world', {9 body: { name: 'World' },10})1112console.log(data) // { message: 'Hello World!' }Expected result: The frontend receives the JSON response from the Edge Function.
Complete working example
1// supabase/functions/hello-world/index.ts2// A complete Edge Function with CORS, auth, and database access34import { createClient } from 'npm:@supabase/supabase-js@2'56const 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}1112Deno.serve(async (req) => {13 // Handle CORS preflight requests14 if (req.method === 'OPTIONS') {15 return new Response('ok', { headers: corsHeaders })16 }1718 try {19 // Parse the request body20 const { name } = await req.json()2122 if (!name) {23 return new Response(24 JSON.stringify({ error: 'Name is required' }),25 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }26 )27 }2829 // Create a Supabase client with the user's auth context30 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 )3940 // Verify the user is authenticated41 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 }4849 // Perform a database operation (RLS applies)50 const { data, error } = await supabase51 .from('greetings')52 .insert({ user_id: user.id, name, greeted_at: new Date().toISOString() })53 .select()54 .single()5556 if (error) {57 return new Response(58 JSON.stringify({ error: error.message }),59 { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }60 )61 }6263 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation