Supabase

Hardening Supabase Edge Functions

Best practices for secure Edge Function development

Last updated 2026-01-15

Why Edge Functions Need Hardening

Supabase Edge Functions run on Deno Deploy and often serve as the secure bridge between your client application and privileged backend operations. They frequently use the service role key to bypass RLS for administrative tasks. A vulnerability in an Edge Function can be just as devastating as a database misconfiguration.

Authentication and Authorization

Verify the JWT

Every Edge Function that handles user requests must verify the incoming JWT:

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

Deno.serve(async (req: Request) => {
  const authHeader = req.headers.get('Authorization');
  if (!authHeader) {
    return new Response(JSON.stringify({ error: 'Missing authorization header' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: authHeader } } }
  );

  const { data: { user }, error } = await supabase.auth.getUser();
  if (error || !user) {
    return new Response(JSON.stringify({ error: 'Invalid token' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // user is now verified, proceed with logic
});

Do not just decode the JWT yourself without verification. Always call getUser() which validates the token against Supabase Auth.

Authorize the Action

Authentication alone is not enough. Verify the user has permission for the specific action:

// Check user has credits before performing a scan
const { data: profile } = await supabaseAdmin
  .from('profiles')
  .select('credits')
  .eq('id', user.id)
  .single();

if (!profile || profile.credits < 1) {
  return new Response(JSON.stringify({ error: 'Insufficient credits' }), {
    status: 403,
    headers: { 'Content-Type': 'application/json' },
  });
}

Input Validation

Never trust client input. Validate everything:

Deno.serve(async (req: Request) => {
  if (req.method !== 'POST') {
    return new Response('Method not allowed', { status: 405 });
  }

  let body: unknown;
  try {
    body = await req.json();
  } catch {
    return new Response(JSON.stringify({ error: 'Invalid JSON' }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // Validate expected fields
  if (typeof body !== 'object' || body === null) {
    return new Response(JSON.stringify({ error: 'Invalid request body' }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  const { url } = body as Record<string, unknown>;
  if (typeof url !== 'string' || !url.startsWith('https://')) {
    return new Response(JSON.stringify({ error: 'Invalid URL' }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // Safe to proceed with validated input
});

Protecting the Service Role Key

The service role key bypasses RLS entirely. Treat it with extreme care:

  1. Never return it to the client -- Not in responses, error messages, or logs
  2. Use it only in Edge Functions, never in client-side code
  3. Create a separate admin client and use it only for operations that truly need elevated privileges
// Admin client for privileged operations
const supabaseAdmin = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);

// Regular client for user-scoped operations
const supabaseUser = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_ANON_KEY')!,
  { global: { headers: { Authorization: authHeader } } }
);

CORS Configuration

Every Edge Function should set proper CORS headers:

const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://yourdomain.com',  // Not '*'
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
  'Access-Control-Allow-Headers': 'authorization, content-type, x-client-info',
};

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

  // ... function logic

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

Avoid using '*' for the Access-Control-Allow-Origin header in production. Specify your exact domain.

Rate Limiting

Edge Functions do not have built-in rate limiting. Implement your own:

// Simple rate limiting using Supabase table
async function checkRateLimit(userId: string, action: string): Promise<boolean> {
  const windowStart = new Date(Date.now() - 60 * 1000).toISOString(); // 1 min window

  const { count } = await supabaseAdmin
    .from('rate_limits')
    .select('*', { count: 'exact', head: true })
    .eq('user_id', userId)
    .eq('action', action)
    .gte('created_at', windowStart);

  return (count ?? 0) < 10; // Max 10 requests per minute
}

Error Handling

Never leak internal details in error responses:

try {
  // ... operation
} catch (err) {
  // Log full error server-side
  console.error('Internal error:', err);

  // Return generic error to client
  return new Response(
    JSON.stringify({ error: 'An internal error occurred' }),
    { status: 500, headers: { 'Content-Type': 'application/json' } }
  );
}

Never expose stack traces, database error messages, or internal paths to the client. These give attackers information about your infrastructure.

Environment Variables

Store all secrets in Supabase Edge Function secrets, never in code:

supabase secrets set STRIPE_SECRET_KEY=sk_live_...
supabase secrets set RESEND_API_KEY=re_...

Access them via Deno.env.get() at runtime. Never commit .env files containing production secrets.

Automated Scanning

AuditYour.app can test your Edge Functions for common vulnerabilities including missing authentication checks, CORS misconfigurations, and exposed error details. Regular automated scanning catches issues before they reach production.

Scan your app for this vulnerability

AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.

Run Free Scan