Supabase — Edge Function Checklist
Required structure, CORS, auth pattern, secrets management, error handling, webhook signature verification.
Back to Supabase Checklists
0/0 completed
Core Rule: Edge Functions are for server-side logic that cannot or should not run in the browser — webhooks, external API calls, payment processing, email sending, and any logic that requires secrets. They are not a replacement for simple table queries or RPCs.
Contents
| # | Section |
|---|---|
| 1 | When to Use an Edge Function |
| 2 | Edge Function Structure |
| 3 | Auth Pattern |
| 4 | Request Validation |
| 5 | Response Standard |
| 6 | Secrets & Environment Variables |
| 7 | Error Handling & Logging |
| 8 | Webhook Handling |
| 9 | Edge Function Checklist — Before Marking Done |
1. When to Use an Edge Function
Use an Edge Function when:
Do NOT use an Edge Function when:
× Simple table query — use supabase.from() with RLS
× Business logic without secrets — use RPC / Postgres Function
× Just to "wrap" a table query for no reason
× The logic could be enforced at the database level (constraints, triggers)
2. Edge Function Structure
Every Edge Function must follow this structure:
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req: Request) => {
// 1. Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// 2. Auth check (if protected)
// 3. Parse and validate input
// 4. Execute business logic
// 5. Return success response
return new Response(
JSON.stringify({ data: result, message: 'Success.' }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 }
);
} catch (error) {
// 6. Handle errors
console.error('[function-name] Error:', error.message);
return new Response(
JSON.stringify({ error: 'SERVER_ERROR', message: 'An unexpected error occurred.' }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500 }
);
}
});
3. Auth Pattern
// For protected Edge Functions — validate auth token first
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'AUTH_REQUIRED', message: 'Authentication required.' }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 401 }
);
}
// Create client with user's token (not service role)
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
);
// Verify token and get user
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return new Response(
JSON.stringify({ error: 'AUTH_REQUIRED', message: 'Invalid or expired token.' }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 401 }
);
}
4. Request Validation
// Parse body
const body = await req.json().catch(() => null);
if (!body) {
return new Response(
JSON.stringify({ error: 'VALIDATION_ERROR', message: 'Invalid request body.' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Validate required fields
const errors: Record<string, string> = {};
if (!body.name || typeof body.name !== 'string' || body.name.trim() === '') {
errors.name = 'Name is required.';
}
if (!body.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
errors.email = 'Enter a valid email address.';
}
if (Object.keys(errors).length > 0) {
return new Response(
JSON.stringify({ error: 'VALIDATION_ERROR', message: 'Validation failed.', fields: errors }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
5. Response Standard
All responses must follow this format:
Success
{
"data": { ... },
"message": "Descriptive success message."
}
Error
{
"error": "ERROR_CODE",
"message": "Human-readable description."
}
Validation Error (with field errors)
{
"error": "VALIDATION_ERROR",
"message": "Validation failed.",
"fields": {
"email": "Enter a valid email address.",
"name": "Name is required."
}
}
6. Secrets & Environment Variables
7. Error Handling & Logging
What to Log
What NOT to Log
× Passwords or tokens
× Full credit card numbers
× Personal data beyond what is needed for debugging (e.g., log user ID, not full name)
× Full request body if it contains sensitive fields
× Supabase service role key or other secrets
Error Pattern
try {
// business logic
} catch (error) {
// Log for debugging
console.error(`[function-name] Unexpected error: ${error.message}`);
// Never expose internal details to client
return new Response(
JSON.stringify({ error: 'SERVER_ERROR', message: 'An unexpected error occurred.' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
8. Webhook Handling
For Edge Functions that receive webhooks from external services:
[ ] Webhook signature verified before processing payload
[ ] Signature secret stored as an environment variable — never hardcoded
[ ] Idempotency check: if the same event arrives twice, only process it once
— Store event_id in a processed_webhooks table
— Check for duplicate before processing
[ ] Respond with 200 immediately if only acknowledgement is needed (queue async work)
[ ] Never trust payload claims without signature verification
[ ] Webhook secret rotated if ever exposed
Signature Verification Example (Stripe)
import Stripe from 'https://esm.sh/stripe@12';
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {
apiVersion: '2023-10-16',
});
const signature = req.headers.get('stripe-signature') ?? '';
const body = await req.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
Deno.env.get('STRIPE_WEBHOOK_SECRET') ?? ''
);
} catch (err) {
return new Response(
JSON.stringify({ error: 'INVALID_SIGNATURE', message: 'Webhook signature verification failed.' }),
{ status: 400 }
);
}
9. Edge Function Checklist — Before Marking Done
JUSTIFICATION
[ ] Edge Function is needed — not a simpler table query or RPC
[ ] Reason documented (requires secrets / webhook / third-party API)
STRUCTURE
[ ] CORS OPTIONS handler at the top
[ ] Try/catch wraps entire logic
[ ] All responses include CORS headers + Content-Type: application/json
AUTH
[ ] Protected functions validate auth token at the start
[ ] Public functions confirmed intentionally public
[ ] User ID from validated JWT — not from request body
[ ] Role from database — not from request body
INPUT VALIDATION
[ ] req.json() wrapped in try/catch
[ ] Required fields validated
[ ] Field-level errors returned for validation failures
[ ] String lengths validated
[ ] Format/enum values validated
RESPONSE FORMAT
[ ] Success: { data, message } with 200/201
[ ] Error: { error, message } with correct status
[ ] Validation: { error, message, fields } with 400
[ ] No stack traces or secrets in response
SECRETS
[ ] All secrets via Deno.env.get()
[ ] No secrets hardcoded
[ ] Secrets added to Supabase Dashboard (not just .env.local)
ERROR HANDLING
[ ] All errors caught and returned with correct status
[ ] Logging includes function name, action, and error details
[ ] No sensitive data in logs
WEBHOOKS (if applicable)
[ ] Signature verified before processing
[ ] Idempotency check implemented
[ ] Returns 200 quickly (async processing if needed)
TESTING
[ ] Success case tested
[ ] Invalid input tested
[ ] Auth failure tested
[ ] Webhook signature rejection tested (if webhook)
[ ] Staged on staging before production
[ ] Secrets confirmed set in the correct Supabase project/environment
Practice Task
Apply what you learned by building a real Edge Function that calls a third-party API and handles every error case.
→ Task 04: Build a Project Invite Edge Function
Covers: full Edge Function structure (CORS/auth/validation/logic/response), Deno.env secrets, JWT validation, permission check, duplicate check, DB insert via service role, email via Resend, structured logging without sensitive data.