Supabase Edge Function Security Checklist
Supabase Edge Functions run on the Deno runtime and serve as the server-side logic layer for your application. Because they can use the service_role key to bypass RLS, they represent a critical security boundary. This checklist covers how to write secure Edge Functions.
1. Authentication and Authorization
Every Edge Function that handles user-specific data must validate the JWT from the Authorization header:
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 authenticated; proceed with user.id
});
Do not use getSession() for authorization. The session's JWT can be tampered with on the client side. Always use getUser() which validates the token against the Supabase Auth server.
2. Service Role Key Usage
The service_role key bypasses all RLS policies. Create a separate Supabase client for privileged operations and use it sparingly:
// Only create this when you need to bypass RLS
const adminClient = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// Always verify the user's permission BEFORE using adminClient
if (user.app_metadata.role !== 'admin') {
return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 });
}
// Now safe to perform privileged operation
const { data } = await adminClient.from('admin_config').select('*');
Never pass the service_role key in responses, logs, or error messages.
3. Input Validation
Validate every field from the request body. Edge Functions receive arbitrary input from the internet:
const body = await req.json().catch(() => null);
if (!body) {
return new Response(JSON.stringify({ error: 'Invalid JSON body' }), { status: 400 });
}
const { url, projectId } = body;
// Validate URL format
if (typeof url !== 'string' || !url.startsWith('https://')) {
return new Response(JSON.stringify({ error: 'Invalid URL' }), { status: 400 });
}
// Validate projectId is a UUID
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(projectId)) {
return new Response(JSON.stringify({ error: 'Invalid project ID' }), { status: 400 });
}
Be especially careful with inputs that are used in:
- Database queries (SQL injection)
- HTTP requests to external services (SSRF)
- File system operations (path traversal)
- Template rendering (injection attacks)
4. CORS Configuration
Set CORS headers on every response, including error responses:
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://yourapp.com',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'authorization, content-type, x-client-info',
};
// Handle preflight
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
Avoid using Access-Control-Allow-Origin: * in production. Restrict it to your application's domain.
5. Secret Management
Store all secrets (API keys, webhook signing secrets, encryption keys) using Supabase secrets:
supabase secrets set STRIPE_SECRET_KEY=sk_live_...
supabase secrets set WEBHOOK_SECRET=whsec_...
Access them via Deno.env.get() at runtime. Never hardcode secrets in function source code.
6. SQL Injection Prevention
When using raw SQL in Edge Functions (via supabase.rpc() or direct queries), always use parameterized queries:
// DANGEROUS: String interpolation
const { data } = await adminClient.rpc('search_users', { query: userInput });
// Make sure the database function uses parameterized queries internally
// SAFE: Use the Supabase client's built-in parameterization
const { data } = await supabase
.from('users')
.select('*')
.ilike('name', `%${sanitizedInput}%`);
7. Error Handling
Never expose internal error details to the client:
try {
// Business logic
} catch (error) {
// Log the full error for debugging
console.error('Function error:', error);
// Return a generic message to the client
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
Use AuditYour.app to scan your Supabase project and ensure your Edge Functions are properly secured.
Scan your app for this vulnerability
AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.
Run Free ScanRelated
checklists
Supabase Security Checklist
Comprehensive security checklist for Supabase projects
checklists
Supabase RLS Audit Checklist
Step-by-step checklist for auditing RLS policies
checklists
API Key Management Checklist
Checklist for proper API key handling and rotation
checklists
Supabase Production Readiness Checklist
Checklist for production-ready Supabase setup