To deploy a Supabase Edge Function, create the function with supabase functions new, write your Deno TypeScript code in supabase/functions/your-function/index.ts, test it locally with supabase functions serve, then deploy with supabase functions deploy your-function. Set production secrets with supabase secrets set, verify the deployment in the Dashboard under Edge Functions, and invoke it from the client with supabase.functions.invoke().
Deploying Server-Side Edge Functions in Supabase
Supabase Edge Functions are server-side TypeScript functions running on a Deno-compatible runtime, deployed globally at the edge for minimal latency. They are ideal for tasks that require server-side logic: processing webhooks, calling third-party APIs with secret keys, running background jobs, and implementing business logic that should not run in the browser. This tutorial walks you through the complete workflow from creation to production deployment.
Prerequisites
- Supabase CLI installed (brew install supabase/tap/supabase or npm install supabase --save-dev)
- A Supabase project linked locally with supabase link
- Docker Desktop running (required for supabase functions serve)
- Basic familiarity with TypeScript and Deno
Step-by-step guide
Create a new Edge Function with the Supabase CLI
Create a new Edge Function with the Supabase CLI
Run supabase functions new followed by the function name to scaffold a new function. This creates a directory under supabase/functions/ with an index.ts entry point. The function name becomes part of the URL endpoint. Use lowercase with hyphens for naming. The scaffolded file contains a basic Deno.serve handler that you will customize.
1# Create a new Edge Function2supabase functions new process-payment34# This creates:5# supabase/functions/process-payment/index.tsExpected result: A new directory supabase/functions/process-payment/ is created with an index.ts file containing a basic Deno.serve handler.
Write the Edge Function with CORS handling
Write the Edge Function with CORS handling
Edge Functions require manual CORS handling, unlike the auto-generated REST API. Create a shared CORS headers file in supabase/functions/_shared/cors.ts and import it in your function. Every function must handle OPTIONS preflight requests and include CORS headers in all responses, including error responses. Use Deno.env.get() to access secrets like SUPABASE_URL and SUPABASE_ANON_KEY, which are available automatically in deployed functions.
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/process-payment/index.ts9import { corsHeaders } from '../_shared/cors.ts';10import { createClient } from 'npm:@supabase/supabase-js@2';1112Deno.serve(async (req) => {13 // Handle CORS preflight14 if (req.method === 'OPTIONS') {15 return new Response('ok', { headers: corsHeaders });16 }1718 try {19 const { amount, currency } = await req.json();2021 // Create Supabase client with the user's JWT22 const supabase = createClient(23 Deno.env.get('SUPABASE_URL')!,24 Deno.env.get('SUPABASE_ANON_KEY')!,25 {26 global: { headers: { Authorization: req.headers.get('Authorization')! } },27 }28 );2930 // Your business logic here31 const result = { status: 'processed', amount, currency };3233 return new Response(JSON.stringify(result), {34 headers: { ...corsHeaders, 'Content-Type': 'application/json' },35 status: 200,36 });37 } catch (err) {38 return new Response(JSON.stringify({ error: err.message }), {39 headers: { ...corsHeaders, 'Content-Type': 'application/json' },40 status: 400,41 });42 }43});Expected result: The function handles CORS preflight, processes JSON input, and returns a response with proper headers.
Test the function locally before deploying
Test the function locally before deploying
Use supabase functions serve to start a local development server with hot reload. This runs the function locally using the Deno runtime and provides a local endpoint you can test with curl or your frontend. Local functions automatically have access to SUPABASE_URL and SUPABASE_ANON_KEY for your linked project. For additional secrets, create a supabase/functions/.env file.
1# Start local function server (requires Docker)2supabase functions serve34# In a separate terminal, test with curl5curl -i --location --request POST \6 'http://localhost:54321/functions/v1/process-payment' \7 --header 'Authorization: Bearer YOUR_ANON_KEY' \8 --header 'Content-Type: application/json' \9 --data '{"amount": 1000, "currency": "usd"}'Expected result: The function responds locally with the expected JSON output. Hot reload picks up code changes automatically.
Set production secrets before deploying
Set production secrets before deploying
Edge Functions in production access secrets via Deno.env.get(). SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, and SUPABASE_DB_URL are provided automatically. For custom secrets like third-party API keys, use supabase secrets set. Secrets are encrypted and injected at runtime — no redeployment needed after updating secrets.
1# Set a single secret2supabase secrets set STRIPE_SECRET_KEY=sk_live_your_key_here34# Set multiple secrets from an .env file5supabase secrets set --env-file ./production.env67# List all secrets (values are masked)8supabase secrets list910# Remove a secret11supabase secrets unset STRIPE_SECRET_KEYExpected result: Secrets are stored encrypted in your Supabase project. The function can access them via Deno.env.get('STRIPE_SECRET_KEY').
Deploy the function to production
Deploy the function to production
Run supabase functions deploy with the function name to push it to production. The CLI bundles the function, uploads it, and makes it available at your project's edge function URL. You can deploy a single function or all functions at once. For webhook endpoints that do not require authentication, add the --no-verify-jwt flag.
1# Deploy a single function2supabase functions deploy process-payment34# Deploy all functions at once5supabase functions deploy67# Deploy without JWT verification (for webhooks)8supabase functions deploy webhook-handler --no-verify-jwt910# The production URL will be:11# https://your-project-ref.supabase.co/functions/v1/process-paymentExpected result: The function is deployed and accessible at https://your-project-ref.supabase.co/functions/v1/process-payment.
Invoke the deployed function from your frontend
Invoke the deployed function from your frontend
Use supabase.functions.invoke() from the JavaScript client to call the deployed function. The client automatically includes the user's JWT for authentication. For anonymous access, pass the anon key. The function receives the Authorization header, which you can use to create a Supabase client inside the function that respects the user's RLS permissions.
1import { createClient } from '@supabase/supabase-js';23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6);78// Invoke the Edge Function9const { data, error } = await supabase.functions.invoke('process-payment', {10 body: { amount: 1000, currency: 'usd' },11});1213if (error) {14 console.error('Function error:', error.message);15} else {16 console.log('Result:', data);17}Expected result: The function executes on the server and returns the result to your frontend application.
Complete working example
1// Supabase Edge Function: process-payment2// Handles payment processing with third-party API integration34import { corsHeaders } from '../_shared/cors.ts';5import { createClient } from 'npm:@supabase/supabase-js@2';67interface PaymentRequest {8 amount: number;9 currency: string;10 description?: string;11}1213Deno.serve(async (req) => {14 // Handle CORS preflight request15 if (req.method === 'OPTIONS') {16 return new Response('ok', { headers: corsHeaders });17 }1819 try {20 // Parse the request body21 const { amount, currency, description }: PaymentRequest = await req.json();2223 // Validate input24 if (!amount || amount <= 0) {25 throw new Error('Amount must be a positive number');26 }27 if (!currency) {28 throw new Error('Currency is required');29 }3031 // Create Supabase client using the caller's JWT32 const authHeader = req.headers.get('Authorization')!;33 const supabase = createClient(34 Deno.env.get('SUPABASE_URL')!,35 Deno.env.get('SUPABASE_ANON_KEY')!,36 { global: { headers: { Authorization: authHeader } } }37 );3839 // Verify the user is authenticated40 const { data: { user }, error: authError } = await supabase.auth.getUser();41 if (authError || !user) {42 return new Response(43 JSON.stringify({ error: 'Unauthorized' }),44 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 401 }45 );46 }4748 // Call third-party payment API (example with fetch)49 const stripeKey = Deno.env.get('STRIPE_SECRET_KEY');50 // ... your payment processing logic here ...5152 // Record the transaction in the database53 const { error: dbError } = await supabase54 .from('transactions')55 .insert({56 user_id: user.id,57 amount,58 currency,59 description: description || 'Payment',60 status: 'completed',61 });6263 if (dbError) throw new Error(`Database error: ${dbError.message}`);6465 return new Response(66 JSON.stringify({ status: 'success', amount, currency }),67 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 }68 );69 } catch (err) {70 return new Response(71 JSON.stringify({ error: err.message }),72 { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400 }73 );74 }75});Common mistakes when deploying a Supabase Edge Function
Why it's a problem: Forgetting to handle the OPTIONS preflight request, causing CORS errors on every frontend call
How to avoid: Add an early return for OPTIONS requests with CORS headers: if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders });
Why it's a problem: Using import_map.json for dependencies instead of the newer deno.json format
How to avoid: Migrate to deno.json. Import maps via CLI flags are deprecated. Use npm: specifiers for npm packages: import { createClient } from 'npm:@supabase/supabase-js@2'.
Why it's a problem: Deploying without setting required secrets, causing runtime errors
How to avoid: Set all required secrets with supabase secrets set before deploying. Default secrets (SUPABASE_URL, SUPABASE_ANON_KEY, etc.) are already available.
Why it's a problem: Not including CORS headers in error responses, which hides the actual error message in the browser
How to avoid: Include corsHeaders in every Response, including catch blocks. Without CORS headers on error responses, the browser shows a generic CORS error.
Best practices
- Create a shared _shared/cors.ts file and import it in every function to ensure consistent CORS handling
- Always test functions locally with supabase functions serve before deploying to production
- Use Deno.env.get() for all secrets and never hardcode API keys in function code
- Validate request input early and return descriptive error messages with appropriate HTTP status codes
- Use the caller's JWT to create a Supabase client inside the function so that RLS policies apply to database operations
- Deploy with --no-verify-jwt only for webhook endpoints that receive requests from external services
- Monitor function logs in the Dashboard under Edge Functions to catch runtime errors
- Keep function bundle size under 20 MB to avoid deployment failures and reduce cold start time
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to deploy a Supabase Edge Function that receives a webhook from Stripe, verifies the signature, and updates a subscriptions table in my database. Walk me through creating the function, handling CORS, setting secrets, deploying, and testing.
Create a Supabase Edge Function in Deno TypeScript that accepts a POST request with a JSON body, authenticates the user via the JWT in the Authorization header, performs a database operation using the Supabase client, and returns a JSON response with proper CORS headers.
Frequently asked questions
How long does it take for an Edge Function deployment to go live?
Deployments typically take 10-30 seconds. The function is available at its URL immediately after the CLI reports success. Check the Dashboard under Edge Functions to confirm the deployment status.
Do I need to redeploy after updating secrets?
No. Secrets are injected at runtime, not build time. After running supabase secrets set, the function picks up the new values on the next invocation without redeployment.
What is the maximum execution time for an Edge Function?
The wall-clock timeout for the initial response is 150 seconds. After sending the initial response, background tasks can run up to 400 seconds on Pro plans. CPU time is limited to 2 seconds per request.
Can I deploy multiple functions at once?
Yes. Run supabase functions deploy without specifying a function name to deploy all functions in the supabase/functions/ directory at once.
Why do I get a BOOT_ERROR when deploying?
BOOT_ERROR means the function could not start. Common causes include syntax errors, imports from outside the supabase/functions/ directory, missing dependencies, or incorrect Deno configuration. Check the function logs in the Dashboard for details.
Can Edge Functions access the database directly?
Yes. Edge Functions have the SUPABASE_DB_URL secret available for direct PostgreSQL connections. However, it is recommended to use the Supabase JS client with the service_role key for server-side operations or the user's JWT for operations that should respect RLS.
Can RapidDev help build and deploy Edge Functions for my project?
Yes. RapidDev can architect, build, and deploy Supabase Edge Functions for use cases like webhook processing, third-party API integration, background jobs, and custom authentication flows.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation