Zum Inhalt springen
Infrastruktur & Technologie

Supabase Edge Functions sicher einsetzen

DevOps-Runbook für Supabase Edge Functions: Webhooks, Signaturen, CORS, Deno-Runtime, Input Validation und Claude-Code-Integration.

Mansoor Ahmed
Mansoor Ahmed
Head of Engineering 16 Min. Lesezeit

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-runtime Container 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.

  1. Supabase Self-Hosting Runbook
  2. Next.js über Supabase sicher betreiben
  3. Supabase Edge Functions sicher einsetzen - dieser Artikel
  4. Trigger.dev Background Jobs sicher betreiben
  5. Claude Code als Sicherheitskontrolle im DevOps-Workflow
  6. 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

KriteriumEdge FunctionNext.js Server ActionTrigger.dev Task
AuslöserExterner Event (Webhook)User-Input (Browser)Programmatisch / Cron
LaufzeitUnter 30 SekundenUnter 10 SekundenBis 5+ Minuten
User-KontextNein (kein JWT)Ja (Session)Nein (service_role)
RLS aktivNein (service_role)Ja (anon Key)Nein (service_role)
Retry-LogikKeine (Provider retried)KeineEingebaut (konfigurierbar)
BeispielStripe WebhookFormular speichernPDF-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 herunterladen

Serien-Inhaltsverzeichnis

Dieser Artikel ist Teil unserer DevOps-Serie für self-hosted App-Stacks.

  1. Supabase Self-Hosting Runbook
  2. Next.js über Supabase sicher betreiben
  3. Supabase Edge Functions sicher einsetzen - dieser Artikel
  4. Trigger.dev Background Jobs sicher betreiben
  5. Claude Code als Sicherheitskontrolle im DevOps-Workflow
  6. 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

Bert Gogolin

Geschäftsführer, Gosign

AI Governance Briefing

Enterprise AI, Regulierung und Infrastruktur - einmal im Monat, direkt von mir.

Kein Spam. Jederzeit abbestellbar. Datenschutzerklärung

Supabase Edge Functions Deno Webhooks Security
Artikel teilen

Häufige Fragen

Wofür eignen sich Supabase Edge Functions?

Edge Functions eignen sich für Webhooks, Integrationen mit externen APIs und signierte Event-Endpunkte. Sie sind keine Alternative zu Next.js Server Actions für Business-Logik und kein Ersatz für Trigger.dev bei Langläufern.

Warum muss jeder Webhook-Endpunkt die Signatur prüfen?

Ohne Signaturprüfung kann jeder beliebige Payloads an den Webhook senden. Ein Angreifer könnte gefälschte Events auslösen, zum Beispiel eine gefälschte Checkout-Bestätigung bei Stripe, ohne dass jemals bezahlt wurde.

Warum brauchen Webhooks kein JWT, aber trotzdem eine Signaturprüfung?

Webhooks werden von externen Services wie Stripe oder GitHub aufgerufen, die kein Supabase JWT haben. Stattdessen signieren sie den Payload mit einem shared Secret (HMAC). Die Edge Function muss diese Signatur prüfen, weil sonst jeder beliebige Payloads senden kann.