supabase-edge-functions
Supabase Edge Functions Best Practices
Comprehensive guide for Supabase Edge Functions development, debugging, and integration with database triggers.
Core Concepts
Authentication Patterns
Edge Functions support two authentication modes:
- JWT Verification (verify_jwt: true) - Default, validates Authorization header contains valid JWT
- Custom Auth (verify_jwt: false) - Function handles auth internally (API keys, webhooks)
Service Role vs Anon Key:
- Anon key (
eyJ..., ~219 chars): JWT for client-side, limited permissions via RLS - Service role key (
sb_secret_..., ~600 chars): Full admin access, bypasses RLS, NEVER expose to client
Database Trigger Integration
When calling Edge Functions from database triggers using pg_net:
SELECT net.http_post(
url := 'https://project.supabase.co/functions/v1/function-name',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || v_service_role_key -- Must be SERVICE ROLE KEY
),
body := jsonb_build_object('run_id', p_run_id)
);
Critical: Database triggers MUST use service role key, not anon key.
Common Issues and Solutions
Issue 1: Auth Token Mismatch
Symptoms:
- Logs show: "Token prefix: eyJ... Expected prefix: sb_secret_..."
- Function returns 401 or auth errors
- Database trigger calls fail silently
Root Cause: Anon key stored instead of service role key in database config.
Solution:
- Get service role key from Dashboard > Settings > API > service_role
- Verify key starts with
sb_secret_and is ~600 characters - Update database config:
UPDATE private.config
SET value = 'sb_secret_YOUR_KEY_HERE'
WHERE key = 'service_role_key';
Verification:
SELECT
key,
LEFT(value, 10) as value_preview,
LENGTH(value) as key_length
FROM private.config
WHERE key = 'service_role_key';
-- Should show: sb_secret_... with length ~600
Issue 2: Function Deployment Errors
Common Errors:
- Import map not found
- Module resolution failures
- Type errors in Deno runtime
Solutions:
Import Maps:
Use deno.json for dependencies:
{
"imports": {
"supabase": "jsr:@supabase/supabase-js@2",
"postgres": "https://deno.land/x/postgres@v0.17.0/mod.ts"
}
}
Type Safety: Import runtime types at top of function:
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
Testing Locally:
# Install Supabase CLI
supabase functions serve function-name --env-file .env.local
# Test with curl
curl -i --location --request POST 'http://localhost:54321/functions/v1/function-name' \
--header 'Authorization: Bearer YOUR_ANON_KEY' \
--header 'Content-Type: application/json' \
--data '{"run_id":"test-uuid"}'
Issue 3: Database Connection from Edge Functions
Pattern:
import { createClient } from "jsr:@supabase/supabase-js@2";
Deno.serve(async (req: Request) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // Service role for admin ops
);
// Now can bypass RLS for admin operations
const { data, error } = await supabase
.from('forecast_runs')
.update({ status: 'processing' })
.eq('id', runId);
});
Environment Variables: Edge Functions automatically have access to:
SUPABASE_URL- Project URLSUPABASE_ANON_KEY- Public anon keySUPABASE_SERVICE_ROLE_KEY- Admin service role key
Issue 4: Debugging Failed Triggers
Check trigger configuration:
SELECT * FROM private.config WHERE key IN ('supabase_url', 'service_role_key');
Check pg_net requests:
SELECT * FROM net._http_response ORDER BY created DESC LIMIT 10;
Check Edge Function logs:
Use Supabase MCP tool get_logs or Dashboard > Edge Functions > Logs
Enable verbose logging in function:
console.log('[function-name] Processing run_id:', runId);
console.log('[function-name] Request headers:', Object.fromEntries(req.headers));
console.log('[function-name] Environment check:', {
hasUrl: !!Deno.env.get('SUPABASE_URL'),
hasServiceKey: !!Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
});
Deployment Workflow
1. Develop Locally
supabase functions serve function-name
2. Test Locally
curl -i --location --request POST 'http://localhost:54321/functions/v1/function-name' \
--header 'Authorization: Bearer ANON_KEY' \
--data '{"test": "data"}'
3. Deploy to Production
supabase functions deploy function-name
4. Verify Deployment
# Check function exists
supabase functions list
# Test production endpoint
curl -i --location --request POST 'https://PROJECT.supabase.co/functions/v1/function-name' \
--header 'Authorization: Bearer ANON_KEY' \
--data '{"test": "data"}'
5. Monitor Logs
# Via CLI
supabase functions logs function-name
# Via Dashboard
Dashboard > Edge Functions > function-name > Logs
File Structure Best Practices
supabase/functions/
├── function-name/
│ ├── index.ts # Main handler
│ ├── deno.json # Import map
│ ├── parsers/ # Domain logic (separate from handler)
│ │ └── csv-parser.ts
│ └── _shared/ # Shared utilities (symlinked)
│ └── validation.ts
Shared Code:
Use _shared/ directory for code reused across functions. Supabase CLI automatically includes it.
Error Handling Pattern
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
Deno.serve(async (req: Request) => {
try {
// Parse request
const body = await req.json();
// Validate input
if (!body.run_id) {
return new Response(
JSON.stringify({ error: 'run_id required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Process
const result = await processData(body.run_id);
// Success response
return new Response(
JSON.stringify({ success: true, data: result }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('[function-name] Error:', error);
return new Response(
JSON.stringify({
error: error.message,
stack: error.stack // Include for debugging, remove in production
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
});
Security Best Practices
- Never expose service role key to client - Only use in Edge Functions or database
- Use RLS policies - Even with service role, validate permissions in function logic
- Validate all inputs - Never trust request data
- Rate limiting - Implement for public endpoints
- CORS configuration - Restrict origins in production
- Secrets management - Use Supabase secrets, not hardcoded values