Supabase Edge Functions sicher einsetzen
DevOps-Runbook für Supabase Edge Functions: Webhooks, Signaturen, CORS, Deno-Runtime, Input Validation und Claude-Code-Integration.
Supabase Edge Functions sind serverseitige TypeScript-Funktionen, die auf dem Deno-Runtime laufen und über den Kong API Gateway erreichbar sind.
Sie sind besonders geeignet für Webhooks, Integrationen mit externen APIs, signierte Event-Endpunkte und leichte serverseitige Logik.
Der häufigste Fehler ist, Edge Functions als zweites Backend-System zu verwenden. Mehr als 40% aller Webhook-Sicherheitsvorfälle entstehen durch fehlende Signaturprüfung (GitGuardian State of Secrets Sprawl 2024). Das führt zu doppelter Business-Logik, unklaren Sicherheitsgrenzen und schwer debuggbaren Architekturen.
Dieses Runbook beschreibt, wie Edge Functions im self-hosted Setup sicher eingesetzt werden. Jeder Schritt enthält eine konkrete Implementierung mit echtem Deno-Code, eine prüfbare Bedingung und ein Failure Scenario.
Hinweis zum Runtime: Supabase Edge Functions basieren auf Deno, nicht auf Node.js. Das betrifft Import-Syntax, Modul-System und verfügbare APIs. Alle Code-Beispiele in diesem Artikel sind Deno-kompatibel. Self-hosted Edge Functions laufen im
supabase/edge-runtimeContainer und sind derzeit noch als Beta gekennzeichnet.
Auf einen Blick - Artikel 3 von 6 der DevOps-Runbook-Serie
- Edge Functions nur als Integrationspunkte (Webhooks, Events), nicht als zweites Backend
- Jeder Webhook-Endpunkt muss die Signatur des sendenden Dienstes prüfen
- CORS explizit konfigurieren (kein Wildcard in Produktion)
- Input Validation mit Zod auf jedem eingehenden Payload
- Langläufer an Trigger.dev delegieren statt in Edge Functions blockieren
Serien-Inhaltsverzeichnis
Diese Anleitung ist Teil unserer DevOps-Runbook-Serie für self-hosted App-Stacks.
- Supabase Self-Hosting Runbook
- Next.js über Supabase sicher betreiben
- Supabase Edge Functions sicher einsetzen - dieser Artikel
- Trigger.dev Background Jobs sicher betreiben
- Claude Code als Sicherheitskontrolle im DevOps-Workflow
- Security Baseline für den gesamten Stack
Artikel 1 beschreibt die Plattform mit der Kong-Konfiguration, über die Edge Functions erreichbar sind. Artikel 2 beschreibt die Next.js App-Schicht mit den Server Actions für Business-Logik. Dieser Artikel beschreibt Integrationen und Webhooks.
Architekturüberblick
Browser
|
Next.js (App-Schicht)
|
+-- Supabase Client (anon key) -> für User-Requests
|
Kong API Gateway
|
+-- PostgREST -> REST API mit RLS
+-- GoTrue -> Auth
+-- Edge Runtime -> Edge Functions
| |
| +-- stripe-webhook
| +-- github-webhook
| +-- trigger-webhook
|
PostgreSQL + RLS Policies
Externe Services
|
+-- Stripe / GitHub / Trigger.dev -> rufen Webhooks auf
Grundregel:
Edge Functions sind Integrationspunkte für externe Events.
Business-Logik gehört nach Next.js (Server Actions / Route Handler).
Langläufer gehören in Trigger.dev.
Entscheidungskriterium
Wenn eine Funktion auf einen externen Event reagiert (Stripe Payment, GitHub Push, Trigger.dev Callback), ist sie eine Edge Function. Wenn sie auf User-Input reagiert und Daten transformiert, gehört sie nach Next.js. Wenn sie länger als 30 Sekunden läuft, gehört sie in Trigger.dev.
Entscheidungstabelle: Edge Function vs. Server Action vs. Trigger.dev
| Kriterium | Edge Function | Next.js Server Action | Trigger.dev Task |
|---|---|---|---|
| Auslöser | Externer Event (Webhook) | User-Input (Browser) | Programmatisch / Cron |
| Laufzeit | Unter 30 Sekunden | Unter 10 Sekunden | Bis 5+ Minuten |
| User-Kontext | Nein (kein JWT) | Ja (Session) | Nein (service_role) |
| RLS aktiv | Nein (service_role) | Ja (anon Key) | Nein (service_role) |
| Retry-Logik | Keine (Provider retried) | Keine | Eingebaut (konfigurierbar) |
| Beispiel | Stripe Webhook | Formular speichern | PDF-Generierung |
Wie Edge Functions im Self-Hosted Setup laufen
Im self-hosted Docker-Compose-Stack sieht die Edge Functions Konfiguration so aus:
# docker-compose.yml (Auszug)
functions:
container_name: supabase-edge-functions
image: supabase/edge-runtime:v1.66.4 # Version pinnen
restart: unless-stopped
depends_on:
- analytics
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://kong:8000 # interner Docker-Hostname
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
volumes:
- ./volumes/functions:/home/deno/functions:Z
command:
- start
- --main-service
- /home/deno/functions/main
Edge Functions liegen als TypeScript-Dateien im Volume:
volumes/functions/
main/
index.ts <- Router / Main Service
_shared/
cors.ts <- CORS Headers (shared)
supabase-client.ts <- Supabase Client Factory (shared)
stripe-webhook/
index.ts
github-webhook/
index.ts
Deployment im Self-Hosted Setup: Funktionen werden als Dateien in das Volume gelegt und der Container neu gestartet:
# Functions in das Volume kopieren
cp -r supabase/functions/* /opt/supabase/volumes/functions/
# Container neu starten
docker compose restart functions --no-deps
Teil A - Architekturentscheidungen
Diese Entscheidungen werden einmal getroffen und selten geändert.
A1 - Edge Functions nur für Integrationen, nicht als zweites Backend
Umsetzung
Klare Zuordnung, welche Logik wohin gehört:
Edge Functions (Deno):
Webhook Empfang (Stripe, GitHub, externe APIs)
Event-basierte Sync (DB Webhook -> externer Service)
Signaturprüfung eingehender Events
Leichte Transformationen (< 30 Sekunden)
Next.js (Server Actions / Route Handler):
User-facing Mutations (CRUD)
Session-basierte Operationen
Business-Logik
Input Validation mit User-Kontext
Trigger.dev (Background Jobs):
PDF-Generierung
AI-Tasks
Massen-E-Mail-Versand
Alles über 30 Sekunden Laufzeit
Prüfbare Bedingung
# Wie viele Edge Functions gibt es?
ls -d volumes/functions/*/ | grep -v "main\|_shared" | wc -l
# Für jede Function prüfen: ist es eine Integration?
for dir in volumes/functions/*/; do
name=$(basename "$dir")
[[ "$name" == "main" || "$name" == "_shared" ]] && continue
echo "--- $name ---"
# Enthält es Webhook/Integration Patterns?
grep -l "signature\|webhook\|stripe\|github\|trigger" "$dir"*.ts 2>/dev/null || \
echo "WARNUNG: keine Webhook/Integration Patterns gefunden"
done
Failure Scenario
Wenn Business-Logik in Edge Functions implementiert wird, entsteht eine Schattenarchitektur: Dieselbe Validierung existiert in Next.js Server Actions UND in Edge Functions, mit subtilen Unterschieden. Bugs werden schwer reproduzierbar, weil unklar ist, welcher Codepfad aktiv war. Auth-Logik muss an zwei Stellen gepflegt werden.
A2 - Webhook-Endpunkte isolieren: eine Funktion pro Webhook
Umsetzung
Jeder Webhook-Provider bekommt eine eigene Funktion in einem eigenen Verzeichnis:
volumes/functions/
stripe-webhook/
index.ts
github-webhook/
index.ts
trigger-webhook/
index.ts
Nicht so:
volumes/functions/
webhooks/
index.ts <- verarbeitet Stripe, GitHub und Trigger in einer Datei
Prüfbare Bedingung
# Jede Webhook-Function sollte genau einen Provider bedienen
for dir in volumes/functions/*-webhook/; do
name=$(basename "$dir")
providers=$(grep -ciE "stripe|github|trigger|slack|sendgrid" "$dir/index.ts")
if [ "$providers" -gt 1 ]; then
echo "WARNUNG: $name bedient mehrere Provider ($providers)"
fi
done
Failure Scenario
Wenn mehrere Webhooks in einer Funktion verarbeitet werden, teilen sie sich denselben Error Handler. Ein fehlerhafter Stripe-Payload kann den GitHub-Webhook blockieren. Retry-Strategien unterscheiden sich pro Provider (Stripe retried mit exponential Backoff, GitHub nur 3x), das lässt sich in einer gemeinsamen Funktion kaum sauber abbilden.
A3 - CORS korrekt konfigurieren
Umsetzung
Edge Functions die vom Browser aufgerufen werden (auch indirekt) brauchen CORS-Header. Supabase liefert keine automatische CORS-Konfiguration für Edge Functions.
Shared CORS Config:
// volumes/functions/_shared/cors.ts
const allowedOrigins = [
'https://app.example.com',
...(Deno.env.get('ENVIRONMENT') === 'development'
? ['http://localhost:3000']
: [])
]
export function getCorsHeaders(req: Request) {
const origin = req.headers.get('origin') ?? ''
const corsOrigin = allowedOrigins.includes(origin) ? origin : ''
return {
'Access-Control-Allow-Origin': corsOrigin,
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
}
}
Jede Edge Function muss CORS am Anfang behandeln:
import { getCorsHeaders } from '../_shared/cors.ts'
Deno.serve(async (req) => {
const corsHeaders = getCorsHeaders(req)
// CORS Preflight muss ERSTE Prüfung sein
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// ... eigentliche Logik
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
})
Prüfbare Bedingung
# Alle Functions müssen CORS behandeln
for dir in volumes/functions/*/; do
name=$(basename "$dir")
[[ "$name" == "main" || "$name" == "_shared" ]] && continue
if ! grep -q "OPTIONS" "$dir/index.ts" 2>/dev/null; then
echo "WARNUNG: $name hat keinen OPTIONS Handler"
fi
done
# Kein Wildcard CORS in Produktion
grep -r "'\\*'" volumes/functions/ --include="*.ts" | grep -i "allow-origin"
# Erwartung: keine Treffer (außer in Development-Branches)
Failure Scenario
Ohne CORS-Header schlagen alle Browser-Requests an Edge Functions fehl. Der Browser blockiert die Response, auch wenn die Funktion korrekt antwortet. Das äußert sich als kryptischer CORS-Fehler in der Konsole. Mit Wildcard-CORS ('*') in Produktion kann jede beliebige Website Requests an eure Edge Functions senden und die Responses lesen.
Teil B - Implementierungschecks
Diese Checks gelten für jede Edge Function und müssen bei jedem Deployment geprüft werden.
B1 - Webhook-Signaturen prüfen
Umsetzung
Jeder Webhook-Endpunkt muss die Signatur des sendenden Services verifizieren. Ohne Signaturprüfung kann jeder beliebige Payloads an die Function senden.
Vollständiges Stripe-Webhook Beispiel:
// volumes/functions/stripe-webhook/index.ts
import { getCorsHeaders } from '../_shared/cors.ts'
const STRIPE_WEBHOOK_SECRET = Deno.env.get('STRIPE_WEBHOOK_SECRET')
Deno.serve(async (req) => {
const corsHeaders = getCorsHeaders(req)
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// 1. Nur POST erlauben
if (req.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// 2. Signatur prüfen
const signature = req.headers.get('stripe-signature')
if (!signature) {
return new Response(
JSON.stringify({ error: 'Missing signature' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
const body = await req.text()
const isValid = await verifyStripeSignature(body, signature, STRIPE_WEBHOOK_SECRET!)
if (!isValid) {
return new Response(
JSON.stringify({ error: 'Invalid signature' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// 3. Event verarbeiten
const event = JSON.parse(body)
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(event.data.object)
break
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object)
break
default:
// Unbekannte Events ignorieren, nicht fehlschlagen
console.log(`Unhandled event type: ${event.type}`)
}
// 4. Immer 200 zurückgeben (Stripe retried sonst)
return new Response(
JSON.stringify({ received: true }),
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
})
// Stripe Signatur Verifizierung mit Deno Crypto API
async function verifyStripeSignature(
payload: string,
header: string,
secret: string
): Promise<boolean> {
const parts = header.split(',')
const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1]
const signature = parts.find(p => p.startsWith('v1='))?.split('=')[1]
if (!timestamp || !signature) return false
// Timing Check: Events älter als 5 Minuten ablehnen
const now = Math.floor(Date.now() / 1000)
if (now - parseInt(timestamp) > 300) return false
const signedPayload = `${timestamp}.${payload}`
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const sig = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(signedPayload)
)
const expectedSig = Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
return expectedSig === signature
}
async function handleCheckoutCompleted(session: Record<string, unknown>) {
console.log(`Checkout completed: ${session.id}`)
}
async function handlePaymentFailed(invoice: Record<string, unknown>) {
console.log(`Payment failed: ${invoice.id}`)
}
GitHub-Webhook Signaturprüfung (anderes Verfahren):
// volumes/functions/github-webhook/index.ts
const GITHUB_WEBHOOK_SECRET = Deno.env.get('GITHUB_WEBHOOK_SECRET')
async function verifyGitHubSignature(
payload: string,
signatureHeader: string,
secret: string
): Promise<boolean> {
// GitHub nutzt sha256=<hex>
const expected = signatureHeader.replace('sha256=', '')
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const sig = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(payload)
)
const computed = Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
return computed === expected
}
Prüfbare Bedingung
# Jede Webhook-Function muss eine Signaturprüfung haben
for dir in volumes/functions/*-webhook/; do
name=$(basename "$dir")
if ! grep -qE "signature|verify|hmac" "$dir/index.ts" 2>/dev/null; then
echo "KRITISCH: $name hat keine Signaturprüfung"
else
echo "OK: $name prüft Signaturen"
fi
done
# VERIFY_JWT Setting prüfen
# Webhooks brauchen VERIFY_JWT=false, weil externe Services kein Supabase JWT senden
grep "VERIFY_JWT" .env
Failure Scenario
Ohne Signaturprüfung kann ein Angreifer beliebige Events an den Webhook senden:
curl -X POST https://app.example.com/functions/v1/stripe-webhook \
-H "Content-Type: application/json" \
-d '{"type":"checkout.session.completed","data":{"object":{"customer":"cus_fake"}}}'
Dieser Request würde eine gefälschte Checkout-Bestätigung auslösen. Die Funktion würde den User-Status aktualisieren, obwohl nie bezahlt wurde.
B2 - Supabase Client korrekt initialisieren (anon vs. service_role)
Umsetzung
Edge Functions haben Zugriff auf alle Supabase Environment Variables. Die Wahl zwischen anon Key und service_role Key ist die wichtigste Sicherheitsentscheidung pro Function.
Shared Client Factory:
// volumes/functions/_shared/supabase-client.ts
import { createClient, SupabaseClient } from 'npm:@supabase/supabase-js@2'
// Client MIT RLS (für User-Kontext Operationen)
export function createAnonClient(authHeader?: string): SupabaseClient {
const client = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{
global: {
headers: authHeader ? { Authorization: authHeader } : {},
},
}
)
return client
}
// Client OHNE RLS (für Webhook Processing, Admin Tasks)
// ACHTUNG: Umgeht alle Row Level Security Policies
export function createAdminClient(): SupabaseClient {
return createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
}
Wann welcher Client:
createAnonClient(authHeader):
User-facing Functions (selten, gehören meist nach Next.js)
Wenn RLS den Zugriff kontrollieren soll
createAdminClient():
Webhook Processing (Stripe, GitHub)
Interne Sync-Tasks
Wenn die Function KEINEN User-Kontext hat
Faustregel: Webhooks haben keinen User-Kontext,
also brauchen sie service_role.
Aber: Nur die minimalen Operationen ausführen.
Prüfbare Bedingung
# Wo wird service_role / Admin Client genutzt?
grep -rn "SERVICE_ROLE\|createAdminClient\|service_role" \
volumes/functions/ --include="*.ts" | grep -v "_shared/"
# Erwartung: nur in Webhook-Functions, nicht in User-facing Functions
# Wird der Admin Client mit ungeprüftem User-Input gefüttert?
# Manueller Review: In jeder Function die createAdminClient nutzt,
# prüfen ob Input vor der DB-Operation validiert wird
Failure Scenario
Wenn eine Edge Function mit service_role läuft und User-Input direkt in Queries weiterreicht, umgeht sie RLS vollständig. Ein manipulierter Webhook-Payload könnte dann beliebige Daten in beliebigen Tabellen lesen oder schreiben, weil der service_role Key keine Einschränkungen kennt.
B3 - Input Validation
Umsetzung
Jede Edge Function muss eingehende Daten validieren, bevor sie verarbeitet werden. In Deno funktioniert Zod über npm-Import:
// volumes/functions/stripe-webhook/index.ts (Auszug)
import { z } from 'npm:zod@3'
// Schema für den erwarteten Stripe Event
const stripeEventSchema = z.object({
id: z.string().startsWith('evt_'),
type: z.string(),
data: z.object({
object: z.record(z.unknown()),
}),
})
// In der Hauptfunktion nach Signaturprüfung:
const parsed = stripeEventSchema.safeParse(JSON.parse(body))
if (!parsed.success) {
return new Response(
JSON.stringify({ error: 'Invalid payload structure' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// Ab hier mit parsed.data arbeiten (typsicher)
const event = parsed.data
Prüfbare Bedingung
# Jede Function sollte Input Validation haben
for dir in volumes/functions/*/; do
name=$(basename "$dir")
[[ "$name" == "main" || "$name" == "_shared" ]] && continue
if grep -qE "zod|safeParse|z\.object|z\.string" "$dir/index.ts" 2>/dev/null; then
echo "OK: $name hat Schema Validation"
elif grep -qE "JSON\.parse" "$dir/index.ts" 2>/dev/null; then
echo "WARNUNG: $name parsed JSON ohne Schema Validation"
fi
done
Failure Scenario
Ohne Input Validation akzeptiert die Function jeden Payload, der eine gültige Signatur hat. Ein kompromittierter API-Key beim Webhook-Provider könnte dann unerwartete Datenstrukturen senden, die zu undefinierten Datenbankoperationen führen, z.B. undefined als User-ID in einen Insert.
B4 - Secrets in Environment Variables, nie im Code
Umsetzung
Im Self-Hosted Setup werden Secrets über die Docker-Compose-Konfiguration an Edge Functions übergeben:
# docker-compose.yml (Auszug)
functions:
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
env_file:
- .env.functions # zusätzliche Secrets für Functions
# .env.functions (nur auf dem Server, nicht im Git)
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_SECRET_KEY=sk_live_...
GITHUB_WEBHOOK_SECRET=ghsec_...
TRIGGER_DEV_API_KEY=tr_...
Im Code zugreifen:
// So: Environment Variable
const secret = Deno.env.get('STRIPE_WEBHOOK_SECRET')
// NIEMALS so: Hardcoded
const secret = 'whsec_abc123...'
Für die allgemeinen Grundlagen der Secret-Verwaltung siehe Datensicherheit in der Enterprise-KI-Infrastruktur.
Prüfbare Bedingung
# Hardcoded Secrets im Code?
grep -rn "sk_live\|sk_test\|whsec_\|ghsec_\|Bearer ey" \
volumes/functions/ --include="*.ts"
# Erwartung: keine Treffer
# .env.functions nicht im Git?
git ls-files .env.functions
# Erwartung: leer
# Dateirechte korrekt?
stat -c "%a" .env.functions
# Erwartung: 600
Failure Scenario
Hardcoded Secrets im Quellcode landen im Git-Repository. Selbst wenn das Repo privat ist, haben alle Entwickler mit Repo-Zugang auch Zugang zu Produktions-Secrets. Bei einem versehentlichen Public-Push sind die Secrets sofort kompromittiert.
B5 - Timeouts und Langläufer vermeiden
Umsetzung
Edge Functions sind für kurze, idempotente Operationen konzipiert. Der Self-Hosted Edge Runtime hat ein Default-Timeout (konfigurierbar, typisch 60 Sekunden für self-hosted). Langläufer blockieren Worker-Slots.
Geeignet (< 30 Sekunden):
Webhook empfangen und DB updaten
Signatur prüfen und Event weiterleiten
Kurze API-Calls an externe Services
NICHT geeignet (Trigger.dev verwenden):
PDF-Generierung
AI-Inference (LLM Calls)
Video/Bild-Verarbeitung
Massen-Datenbank-Operationen
E-Mail-Versand an viele Empfänger
Pattern für Langläufer: Edge Function als Trigger, Job in Trigger.dev:
// volumes/functions/process-document/index.ts
// RICHTIG: Edge Function triggert nur den Job
Deno.serve(async (req) => {
// ... Signatur prüfen, Input validieren ...
// Langläufer-Job an Trigger.dev delegieren
const triggerResponse = await fetch(
`${Deno.env.get('TRIGGER_DEV_URL')}/api/v1/tasks/process-document/trigger`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('TRIGGER_DEV_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
payload: { documentId: parsed.data.documentId },
}),
}
)
// Sofort antworten, der Job läuft im Hintergrund
return new Response(
JSON.stringify({ queued: true }),
{ status: 202, headers: { 'Content-Type': 'application/json' } }
)
})
Prüfbare Bedingung
# Sucht nach Patterns die auf Langläufer hindeuten
grep -rn "await.*fetch.*openai\|pdf\|sharp\|ffmpeg\|sleep\|setTimeout" \
volumes/functions/ --include="*.ts" | grep -v "trigger"
# Erwartung: keine Treffer (außer kurze API Calls)
# Gibt es Timeouts im Code?
grep -rn "AbortSignal.timeout\|setTimeout" \
volumes/functions/ --include="*.ts"
# Jeder externe fetch sollte einen Timeout haben
Failure Scenario
Eine Edge Function die 2 Minuten auf einen AI-API-Call wartet, blockiert einen Worker-Slot im Edge Runtime Container. Bei mehreren gleichzeitigen Requests laufen die Worker voll, und nachfolgende Webhook-Calls (z.B. von Stripe) scheitern mit Timeouts. Stripe interpretiert das als Fehler und retried, was die Situation verschlimmert.
B6 - Error Handling und sichere Responses
Umsetzung
Edge Functions dürfen bei Fehlern keine internen Details an den Aufrufer leaken.
// Pattern für sicheres Error Handling
Deno.serve(async (req) => {
const corsHeaders = getCorsHeaders(req)
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// ... eigentliche Logik ...
return new Response(
JSON.stringify({ success: true }),
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (error) {
// Intern loggen (Details)
console.error(`Function error: ${error.message}`, {
stack: error.stack,
// KEINE Secrets oder User-Daten loggen
})
// Extern antworten (generisch)
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
})
Was NICHT in Responses oder Logs gehört:
// FALSCH: Stack Trace an Client senden
return new Response(JSON.stringify({ error: error.stack }), { status: 500 })
// FALSCH: Secrets loggen
console.log(`Connecting with key: ${Deno.env.get('SERVICE_ROLE_KEY')}`)
// FALSCH: Vollständige User-Daten loggen
console.log(`Processing user: ${JSON.stringify(user)}`)
Prüfbare Bedingung
# Werden Error Details an den Client gesendet?
grep -rn "error\.stack\|error\.message" volumes/functions/ --include="*.ts" | \
grep "Response"
# Erwartung: keine Treffer (stack/message nur in console.error)
# Werden Secrets geloggt?
grep -rn "console\.log.*KEY\|console\.log.*SECRET\|console\.log.*token" \
volumes/functions/ --include="*.ts"
# Erwartung: keine Treffer
Failure Scenario
Wenn ein Error Stack Trace an den Client gesendet wird, sieht ein Angreifer interne Pfade, Modulnamen und Datenbankverbindungsdetails. Das erleichtert gezielte Angriffe erheblich. Wenn Secrets in Logs landen, sind sie für jeden mit Monitoring-Zugang sichtbar.
Teil C - Betrieb und Überwachung
C1 - Deployment-Workflow
Umsetzung
Im Self-Hosted Setup gibt es keinen supabase functions deploy Befehl. Der Workflow ist dateibasiert:
#!/bin/bash
# scripts/deploy-functions.sh
set -euo pipefail
FUNCTIONS_DIR="/opt/supabase/volumes/functions"
SOURCE_DIR="./supabase/functions"
echo "Deploying Edge Functions..."
# 1. Syntax-Check (Deno)
for file in $(find "$SOURCE_DIR" -name "*.ts" -not -path "*/_shared/*"); do
deno check "$file" 2>/dev/null || {
echo "FEHLER: Syntax-Fehler in $file"
exit 1
}
done
# 2. Functions kopieren
rsync -av --delete \
--exclude='*.test.ts' \
"$SOURCE_DIR/" "$FUNCTIONS_DIR/"
# 3. Container neu starten
docker compose restart functions --no-deps
# 4. Health Check
sleep 5
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" \
http://localhost:8000/functions/v1/hello 2>/dev/null || echo "000")
if [ "$HEALTH" = "200" ] || [ "$HEALTH" = "401" ]; then
echo "Edge Functions deployment erfolgreich"
else
echo "WARNUNG: Health Check Status $HEALTH"
fi
Prüfbare Bedingung
# Edge Runtime Container läuft?
docker compose ps functions --format '{{.State}}'
# Erwartung: running
# Functions sind erreichbar?
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${ANON_KEY}" \
http://localhost:8000/functions/v1/stripe-webhook
# Erwartung: 200 oder 405 (Method Not Allowed, weil GET statt POST)
C2 - Claude Code Integration
Claude Code prüft Edge Functions kontextuell als Ergänzung zu den deterministischen Checks.
Architektur
Git Push / PR
|
+-- Deterministische Checks (CI/CD)
| +-- grep nach hardcoded Secrets
| +-- grep nach fehlender Signaturprüfung
| +-- grep nach fehlender Input Validation
| +-- grep nach Error Details in Responses
| +-- Deno Type Check
|
+-- Claude Code Analyse (wöchentlich oder bei PR)
+-- Neue Functions ohne Signaturprüfung?
+-- service_role Nutzung angemessen?
+-- Langläufer-Patterns erkannt?
+-- Architektur-Drift (Business-Logik in Functions)?
+-- CORS korrekt für neue Functions?
Konkretes CI-Script
#!/bin/bash
# scripts/check-edge-functions.sh
REPORT=""
FUNCTIONS_DIR="volumes/functions"
# 1. Hardcoded Secrets
SECRETS=$(grep -rn "sk_live\|sk_test\|whsec_\|Bearer ey" \
"$FUNCTIONS_DIR" --include="*.ts" 2>/dev/null)
if [ -n "$SECRETS" ]; then
REPORT+="KRITISCH: Hardcoded Secrets gefunden:\n$SECRETS\n\n"
fi
# 2. Webhook Functions ohne Signaturprüfung
for dir in "$FUNCTIONS_DIR"/*-webhook/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
if ! grep -qE "signature|verify|hmac|crypto" "$dir/index.ts" 2>/dev/null; then
REPORT+="KRITISCH: $name hat keine Signaturprüfung\n"
fi
done
# 3. Functions ohne Input Validation
for dir in "$FUNCTIONS_DIR"/*/; do
name=$(basename "$dir")
[[ "$name" == "main" || "$name" == "_shared" ]] && continue
if grep -q "JSON.parse" "$dir/index.ts" 2>/dev/null && \
! grep -qE "zod|safeParse|z\." "$dir/index.ts" 2>/dev/null; then
REPORT+="WARNUNG: $name parsed JSON ohne Schema Validation\n"
fi
done
# 4. Error Details in Responses
LEAKS=$(grep -rn "error\.stack\|error\.message" "$FUNCTIONS_DIR" --include="*.ts" | \
grep "Response" 2>/dev/null)
if [ -n "$LEAKS" ]; then
REPORT+="WARNUNG: Error Details in Responses:\n$LEAKS\n\n"
fi
# 5. CORS Wildcards
WILDCARDS=$(grep -rn "'\\*'" "$FUNCTIONS_DIR" --include="*.ts" | grep -i "origin" 2>/dev/null)
if [ -n "$WILDCARDS" ]; then
REPORT+="WARNUNG: CORS Wildcard gefunden:\n$WILDCARDS\n\n"
fi
# 6. Langläufer-Patterns
LONG=$(grep -rn "openai\|sharp\|ffmpeg\|puppeteer" "$FUNCTIONS_DIR" --include="*.ts" 2>/dev/null)
if [ -n "$LONG" ]; then
REPORT+="WARNUNG: Mögliche Langläufer-Patterns:\n$LONG\n\n"
fi
# Ausgabe
if [ -n "$REPORT" ]; then
echo -e "=== Edge Function Security Check ===\n"
echo -e "$REPORT"
else
echo "Alle Edge Function Checks bestanden."
fi
Claude führt keine automatischen Änderungen auf Production aus.
Deployment-Checkliste
Vor jedem Deployment von Edge Functions prüfen:
Architektur
[ ] Function ist eine Integration (Webhook/Event), keine Business-Logik
[ ] Ein Webhook-Provider pro Function
[ ] Langläufer an Trigger.dev delegiert
Sicherheit
[ ] Webhook-Signatur wird geprüft
[ ] Input wird mit Schema validiert (z.B. Zod)
[ ] Korrekte Client-Wahl (anon vs. service_role)
[ ] service_role nur wenn kein User-Kontext vorhanden
Secrets
[ ] Keine hardcoded Secrets im Code
[ ] Secrets in .env.functions (nicht im Git)
[ ] .env.functions mit Rechten 600
CORS
[ ] OPTIONS Handler vorhanden
[ ] Kein Wildcard-Origin in Produktion
[ ] CORS Headers in ALLEN Responses (auch Error)
Error Handling
[ ] Try/Catch um die gesamte Logik
[ ] Generische Fehlermeldungen an den Client
[ ] Details nur in console.error (ohne Secrets)
Deployment
[ ] Deno Type Check bestanden
[ ] Container nach Deployment neu gestartet
[ ] Health Check nach Deployment erfolgreich
Fazit
Supabase Edge Functions sind ein leistungsfähiger Integrationspunkt, wenn sie als solcher eingesetzt werden: Webhooks empfangen, Signaturen prüfen, Events verarbeiten und weiterleiten.
Der entscheidende Punkt ist die Abgrenzung. Edge Functions sind kein zweites Backend neben Next.js und kein Job Runner neben Trigger.dev. Wer diese Grenze klar zieht und die Security-Basics umsetzt (Signaturen, Input Validation, korrekter Client, CORS), kann Edge Functions sicher im Self-Hosted Setup betreiben.
Die Kombination aus deterministischen Checks im CI und kontextueller Claude Code Analyse deckt sowohl bekannte Patterns als auch neue, unerwartete Risiken ab. Wer von Anfang an auf Cert-Ready-by-Design setzt, spart sich nachträgliche Audit-Runden.
Audit-Checkliste als Download
Vorbereiteter Prompt für Claude Code. Laden Sie die Datei auf Ihren Server und starten Sie Claude Code im Projektverzeichnis Ihrer Edge Functions. Claude Code prüft automatisch alle Sicherheitspunkte aus diesem Runbook und meldet BESTANDEN, WARNUNG oder KRITISCH.
claude -p "$(cat claude-check-artikel-3-edge-functions.md)" --allowedTools Read,Grep,Glob,Bash
Checkliste herunterladenSerien-Inhaltsverzeichnis
Dieser Artikel ist Teil unserer DevOps-Serie für self-hosted App-Stacks.
- Supabase Self-Hosting Runbook
- Next.js über Supabase sicher betreiben
- Supabase Edge Functions sicher einsetzen - dieser Artikel
- Trigger.dev Background Jobs sicher betreiben
- Claude Code als Sicherheitskontrolle im DevOps-Workflow
- Security Baseline für den gesamten Stack
Im nächsten Artikel zeigen wir, wie Trigger.dev Background Jobs mit Idempotenz und Concurrency Limits sicher betrieben werden, ohne neue Sicherheitsrisiken im Stack zu erzeugen.

Bert Gogolin
Geschäftsführer, Gosign
AI Governance Briefing
Enterprise AI, Regulierung und Infrastruktur - einmal im Monat, direkt von mir.