# How to Deploy a Supabase Edge Function

- Tool: Supabase
- Difficulty: Advanced
- Time required: 15-20 min
- Compatibility: Supabase (all plans), Supabase CLI v1.50+, Deno runtime
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
# Create a new Edge Function
supabase functions new process-payment

# This creates:
# supabase/functions/process-payment/index.ts
```

**Expected result:** A new directory supabase/functions/process-payment/ is created with an index.ts file containing a basic Deno.serve handler.

### 2. 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.

```
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
};

// supabase/functions/process-payment/index.ts
import { corsHeaders } from '../_shared/cors.ts';
import { createClient } from 'npm:@supabase/supabase-js@2';

Deno.serve(async (req) => {
  // Handle CORS preflight
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders });
  }

  try {
    const { amount, currency } = await req.json();

    // Create Supabase client with the user's JWT
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL')!,
      Deno.env.get('SUPABASE_ANON_KEY')!,
      {
        global: { headers: { Authorization: req.headers.get('Authorization')! } },
      }
    );

    // Your business logic here
    const result = { status: 'processed', amount, currency };

    return new Response(JSON.stringify(result), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      status: 200,
    });
  } catch (err) {
    return new Response(JSON.stringify({ error: err.message }), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      status: 400,
    });
  }
});
```

**Expected result:** The function handles CORS preflight, processes JSON input, and returns a response with proper headers.

### 3. 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.

```
# Start local function server (requires Docker)
supabase functions serve

# In a separate terminal, test with curl
curl -i --location --request POST \
  'http://localhost:54321/functions/v1/process-payment' \
  --header 'Authorization: Bearer YOUR_ANON_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"amount": 1000, "currency": "usd"}'
```

**Expected result:** The function responds locally with the expected JSON output. Hot reload picks up code changes automatically.

### 4. 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.

```
# Set a single secret
supabase secrets set STRIPE_SECRET_KEY=sk_live_your_key_here

# Set multiple secrets from an .env file
supabase secrets set --env-file ./production.env

# List all secrets (values are masked)
supabase secrets list

# Remove a secret
supabase secrets unset STRIPE_SECRET_KEY
```

**Expected result:** Secrets are stored encrypted in your Supabase project. The function can access them via Deno.env.get('STRIPE_SECRET_KEY').

### 5. 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.

```
# Deploy a single function
supabase functions deploy process-payment

# Deploy all functions at once
supabase functions deploy

# Deploy without JWT verification (for webhooks)
supabase functions deploy webhook-handler --no-verify-jwt

# The production URL will be:
# https://your-project-ref.supabase.co/functions/v1/process-payment
```

**Expected result:** The function is deployed and accessible at https://your-project-ref.supabase.co/functions/v1/process-payment.

### 6. 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.

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

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// Invoke the Edge Function
const { data, error } = await supabase.functions.invoke('process-payment', {
  body: { amount: 1000, currency: 'usd' },
});

if (error) {
  console.error('Function error:', error.message);
} else {
  console.log('Result:', data);
}
```

**Expected result:** The function executes on the server and returns the result to your frontend application.

## Complete code example

File: `supabase/functions/process-payment/index.ts`

```typescript
// Supabase Edge Function: process-payment
// Handles payment processing with third-party API integration

import { corsHeaders } from '../_shared/cors.ts';
import { createClient } from 'npm:@supabase/supabase-js@2';

interface PaymentRequest {
  amount: number;
  currency: string;
  description?: string;
}

Deno.serve(async (req) => {
  // Handle CORS preflight request
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders });
  }

  try {
    // Parse the request body
    const { amount, currency, description }: PaymentRequest = await req.json();

    // Validate input
    if (!amount || amount <= 0) {
      throw new Error('Amount must be a positive number');
    }
    if (!currency) {
      throw new Error('Currency is required');
    }

    // Create Supabase client using the caller's JWT
    const authHeader = req.headers.get('Authorization')!;
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL')!,
      Deno.env.get('SUPABASE_ANON_KEY')!,
      { global: { headers: { Authorization: authHeader } } }
    );

    // Verify the user is authenticated
    const { data: { user }, error: authError } = await supabase.auth.getUser();
    if (authError || !user) {
      return new Response(
        JSON.stringify({ error: 'Unauthorized' }),
        { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 401 }
      );
    }

    // Call third-party payment API (example with fetch)
    const stripeKey = Deno.env.get('STRIPE_SECRET_KEY');
    // ... your payment processing logic here ...

    // Record the transaction in the database
    const { error: dbError } = await supabase
      .from('transactions')
      .insert({
        user_id: user.id,
        amount,
        currency,
        description: description || 'Payment',
        status: 'completed',
      });

    if (dbError) throw new Error(`Database error: ${dbError.message}`);

    return new Response(
      JSON.stringify({ status: 'success', amount, currency }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 }
    );
  } catch (err) {
    return new Response(
      JSON.stringify({ error: err.message }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400 }
    );
  }
});
```

## Common mistakes

- **Forgetting to handle the OPTIONS preflight request, causing CORS errors on every frontend call** — undefined Fix: Add an early return for OPTIONS requests with CORS headers: if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders });
- **Using import_map.json for dependencies instead of the newer deno.json format** — undefined Fix: 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'.
- **Deploying without setting required secrets, causing runtime errors** — undefined Fix: Set all required secrets with supabase secrets set before deploying. Default secrets (SUPABASE_URL, SUPABASE_ANON_KEY, etc.) are already available.
- **Not including CORS headers in error responses, which hides the actual error message in the browser** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-deploy-a-supabase-edge-function
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-deploy-a-supabase-edge-function
