Przejdź do treści
Infrastruktura & Technologia

Supabase Edge Functions - bezpieczne wdrożenie

Runbook DevOps dla Supabase Edge Functions: webhooki, sygnatury, CORS, Deno runtime, walidacja danych i integracja z Claude Code.

Mansoor Ahmed
Mansoor Ahmed
Head of Engineering 16 min czytania

Supabase Edge Functions to serwerowe funkcje TypeScript działające w runtime Deno, dostępne przez Kong API Gateway.

Szczególnie dobrze nadają się do obsługi webhooków, integracji z zewnętrznymi API, sygnowanych endpointów zdarzeń i lekkiej logiki serwerowej.

Najczęstszym błędem jest wykorzystywanie Edge Functions jako drugiego systemu backendowego. Prowadzi to do podwójnej logiki biznesowej, niejasnych granic bezpieczeństwa i architektur trudnych do debugowania.

Ten runbook opisuje, jak bezpiecznie wdrażać Edge Functions w setup self-hosted. Każdy krok zawiera konkretną implementację z rzeczywistym kodem Deno, sprawdzalny warunek i scenariusz awarii.

Uwaga dotycząca runtime: Supabase Edge Functions bazują na Deno, a nie na Node.js. Dotyczy to składni importów, systemu modułów i dostępnych API. Wszystkie przykłady kodu w tym artykule są kompatybilne z Deno. Self-hosted Edge Functions działają w kontenerze supabase/edge-runtime i są obecnie oznaczone jako wersja beta.

W skrócie - Artykuł 3 z 6 serii DevOps Runbook

  • Edge Functions tylko jako punkty integracyjne (webhooki), nie drugi backend
  • Obowiązkowa weryfikacja sygnatur webhooków
  • CORS skonfigurowany jawnie (bez wildcard)
  • Walidacja danych wejściowych z Zod
  • Zadania długotrwałe delegowane do Trigger.dev

Spis treści serii

Ten przewodnik jest częścią naszej serii runbooków DevOps dla self-hosted stacków aplikacyjnych.

  1. Supabase Self-Hosting Runbook
  2. Next.js nad Supabase - bezpieczna eksploatacja
  3. Supabase Edge Functions - bezpieczne wdrożenie ← ten artykuł
  4. Bezpieczna obsługa Trigger.dev Background Jobs
  5. Claude Code jako kontrola bezpieczeństwa w DevOps
  6. Security Baseline dla całego stacku

Artykuł 1 opisuje platformę. Artykuł 2 opisuje warstwę aplikacyjną Next.js. Ten artykuł opisuje integracje i webhooki.

Przegląd architektury

Browser
   |
Next.js (warstwa aplikacyjna)
   |
   +-- Supabase Client (anon key)     -> dla żądań użytkowników
   |
Kong API Gateway
   |
   +-- PostgREST                       -> REST API z RLS
   +-- GoTrue                          -> Auth
   +-- Edge Runtime                    -> Edge Functions
   |     |
   |     +-- stripe-webhook
   |     +-- github-webhook
   |     +-- trigger-webhook
   |
PostgreSQL + RLS Policies

Usługi zewnętrzne
   |
   +-- Stripe / GitHub / Trigger.dev   -> wywołują webhooki

Podstawowa zasada:

Edge Functions to punkty integracyjne dla zewnętrznych zdarzeń.
Logika biznesowa należy do [Next.js](/pl/magazyn/nextjs-supabase-bezpieczna-konfiguracja/) (Server Actions / Route Handler).
Zadania długotrwałe należą do Trigger.dev.

Tabela decyzyjna

KryteriumEdge FunctionServer ActionTrigger.dev Task
ŹródłoZewnętrzne zdarzenie (webhook)Dane wejściowe użytkownikaZadanie wewnętrzne
Czas wykonania< 30 sekund< 10 sekundDo 5 minut+
UwierzytelnianieSygnatura HMACSesja użytkownika (JWT)Klucz API / service_role
RLSOmijany (service_role)Aktywny (anon key)Omijany (service_role)
RetryDostawca webhookBrak (synchronicznie)Wbudowany (Trigger.dev)
PrzykładStripe checkout webhookFormularz profiluGenerowanie PDF

Kryterium decyzyjne

Jeśli funkcja reaguje na zewnętrzne zdarzenie (płatność Stripe, push GitHub, callback Trigger.dev), jest Edge Function. Jeśli reaguje na dane wejściowe użytkownika i transformuje dane, należy do Next.js. Jeśli trwa dłużej niż 30 sekund, należy do Trigger.dev.

Jak działają Edge Functions w setup self-hosted

W self-hosted stacku Docker Compose konfiguracja Edge Functions wygląda następująco:

# docker-compose.yml (fragment)
functions:
  container_name: supabase-edge-functions
  image: supabase/edge-runtime:v1.66.4    # wersja przypięta
  restart: unless-stopped
  depends_on:
    - analytics
  environment:
    JWT_SECRET: ${JWT_SECRET}
    SUPABASE_URL: http://kong:8000                    # wewnętrzny hostname Docker
    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 to pliki TypeScript w wolumenie:

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 w setup self-hosted: Funkcje umieszcza się jako pliki w wolumenie i restartuje kontener:

# Kopiowanie funkcji do wolumenu
cp -r supabase/functions/* /opt/supabase/volumes/functions/

# Restart kontenera
docker compose restart functions --no-deps

Część A - Decyzje architektoniczne

Te decyzje podejmowane są raz i rzadko ulegają zmianom.

A1 - Edge Functions tylko do integracji, nie jako drugi backend

Implementacja

Jasne przypisanie, jaka logika trafia gdzie:

Edge Functions (Deno):
  Odbiór webhooków (Stripe, GitHub, zewnętrzne API)
  Synchronizacja oparta na zdarzeniach (DB Webhook -> usługa zewnętrzna)
  Weryfikacja sygnatur przychodzących zdarzeń
  Lekkie transformacje (< 30 sekund)

Next.js (Server Actions / Route Handler):
  Mutacje skierowane do użytkownika (CRUD)
  Operacje oparte na sesji
  Logika biznesowa
  Walidacja danych z kontekstem użytkownika

Trigger.dev (Background Jobs):
  Generowanie PDF
  Zadania AI
  Masowa wysyłka e-mail
  Wszystko powyżej 30 sekund czasu wykonania

Sprawdzalny warunek

# Ile jest Edge Functions?
ls -d volumes/functions/*/ | grep -v "main\|_shared" | wc -l

# Dla każdej funkcji sprawdź: czy to integracja?
for dir in volumes/functions/*/; do
  name=$(basename "$dir")
  [[ "$name" == "main" || "$name" == "_shared" ]] && continue
  echo "--- $name ---"
  # Czy zawiera wzorce Webhook/Integration?
  grep -l "signature\|webhook\|stripe\|github\|trigger" "$dir"*.ts 2>/dev/null || \
    echo "OSTRZEŻENIE: nie znaleziono wzorców Webhook/Integration"
done

Scenariusz awarii

Jeśli logika biznesowa zostanie zaimplementowana w Edge Functions, powstaje architektura-cień: ta sama walidacja istnieje w Next.js Server Actions ORAZ w Edge Functions, z subtelnymi różnicami. Błędy stają się trudne do odtworzenia, ponieważ nie jest jasne, która ścieżka kodu była aktywna. Logika autentykacji musi być utrzymywana w dwóch miejscach.

A2 - Izolowanie endpointów webhookowych: jedna funkcja na webhook

Implementacja

Każdy dostawca webhooków otrzymuje własną funkcję w osobnym katalogu:

volumes/functions/
  stripe-webhook/
    index.ts
  github-webhook/
    index.ts
  trigger-webhook/
    index.ts

Nie tak:

volumes/functions/
  webhooks/
    index.ts     <- obsługuje Stripe, GitHub i Trigger w jednym pliku

Sprawdzalny warunek

# Każda funkcja webhookowa powinna obsługiwać dokładnie jednego dostawcę
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 "OSTRZEŻENIE: $name obsługuje wielu dostawców ($providers)"
  fi
done

Scenariusz awarii

Gdy wiele webhooków jest przetwarzanych w jednej funkcji, współdzielą ten sam Error Handler. Wadliwy payload Stripe może zablokować webhook GitHub. Strategie ponawiania różnią się w zależności od dostawcy (Stripe ponawia z exponential backoff, GitHub tylko 3x), co trudno czysto zaimplementować we wspólnej funkcji.

A3 - Poprawna konfiguracja CORS

Implementacja

Edge Functions wywoływane z przeglądarki (także pośrednio) potrzebują nagłówków CORS. Supabase nie zapewnia automatycznej konfiguracji CORS dla Edge Functions.

Wspólna konfiguracja CORS:

// 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',
  }
}

Każda Edge Function musi obsługiwać CORS na początku:

import { getCorsHeaders } from '../_shared/cors.ts'

Deno.serve(async (req) => {
  const corsHeaders = getCorsHeaders(req)

  // CORS Preflight musi być PIERWSZYM sprawdzeniem
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  // ... właściwa logika

  return new Response(JSON.stringify(data), {
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
})

Sprawdzalny warunek

# Wszystkie funkcje muszą obsługiwać CORS
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 "OSTRZEŻENIE: $name nie ma handlera OPTIONS"
  fi
done

# Brak wildcard CORS w produkcji
grep -r "'\\*'" volumes/functions/ --include="*.ts" | grep -i "allow-origin"
# Oczekiwanie: brak wyników (chyba że w branchach developerskich)

Scenariusz awarii

Bez nagłówków CORS wszystkie żądania z przeglądarki do Edge Functions zawiodą. Przeglądarka zablokuje odpowiedź, nawet jeśli funkcja odpowiedziała poprawnie. Objawia się to jako kryptyczny błąd CORS w konsoli. Z wildcard CORS ('*') w produkcji dowolna strona może wysyłać żądania do Edge Functions i odczytywać odpowiedzi.

Część B - Kontrole implementacyjne

Te kontrole obowiązują dla każdej Edge Function i muszą być weryfikowane przy każdym deploymencie.

B1 - Weryfikacja sygnatur webhooków

Implementacja

Każdy endpoint webhookowy musi weryfikować sygnaturę usługi wysyłającej. Bez weryfikacji sygnatury dowolna osoba może wysyłać dowolne payloady do funkcji.

Kompletny przykład Stripe Webhook:

// 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. Tylko POST dozwolony
  if (req.method !== 'POST') {
    return new Response(
      JSON.stringify({ error: 'Method not allowed' }),
      { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }

  // 2. Weryfikacja sygnatury
  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. Przetwarzanie zdarzenia
  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:
      // Nieznane zdarzenia ignorujemy, nie powodujemy błędu
      console.log(`Unhandled event type: ${event.type}`)
  }

  // 4. Zawsze zwracamy 200 (Stripe ponawia w przeciwnym razie)
  return new Response(
    JSON.stringify({ received: true }),
    { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  )
})

// Weryfikacja sygnatury Stripe za pomocą 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

  // Sprawdzenie czasu: odrzucamy zdarzenia starsze niż 5 minut
  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}`)
}

Weryfikacja sygnatury GitHub Webhook (inny mechanizm):

// 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 używa 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
}

Sprawdzalny warunek

# Każda funkcja webhookowa musi mieć weryfikację sygnatury
for dir in volumes/functions/*-webhook/; do
  name=$(basename "$dir")
  if ! grep -qE "signature|verify|hmac" "$dir/index.ts" 2>/dev/null; then
    echo "KRYTYCZNE: $name nie ma weryfikacji sygnatury"
  else
    echo "OK: $name weryfikuje sygnatury"
  fi
done

# Sprawdzenie ustawienia VERIFY_JWT
# Webhooki potrzebują VERIFY_JWT=false, ponieważ zewnętrzne usługi nie wysyłają JWT Supabase
grep "VERIFY_JWT" .env

Scenariusz awarii

Bez weryfikacji sygnatury atakujący może wysyłać dowolne zdarzenia do webhooka:

Statystyka: Zgodnie z danymi Stripe ponad 12% integracji webhookowych nie weryfikuje sygnatur - co czyni je podatnymi na sfałszowane zdarzenia.

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"}}}'

To żądanie spowodowałoby sfałszowane potwierdzenie płatności. Funkcja zaktualizowałaby status użytkownika, mimo że faktycznie nie doszło do zapłaty.

B2 - Poprawna inicjalizacja klienta Supabase (anon vs. service_role)

Implementacja

Edge Functions mają dostęp do wszystkich zmiennych środowiskowych Supabase. Wybór między kluczem anon a kluczem service_role to najważniejsza decyzja bezpieczeństwa dla każdej funkcji.

Współdzielona fabryka klientów:

// volumes/functions/_shared/supabase-client.ts

import { createClient, SupabaseClient } from 'npm:@supabase/supabase-js@2'

// Klient Z RLS (dla operacji w kontekście użytkownika)
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
}

// Klient BEZ RLS (dla przetwarzania webhooków, zadań administracyjnych)
// UWAGA: Omija wszystkie Row Level Security Policies
export function createAdminClient(): SupabaseClient {
  return createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )
}

Kiedy jaki klient:

createAnonClient(authHeader):
  Funkcje skierowane do użytkownika (rzadko, zwykle należą do Next.js)
  Gdy RLS powinien kontrolować dostęp

createAdminClient():
  Przetwarzanie webhooków (Stripe, GitHub)
  Wewnętrzne zadania synchronizacji
  Gdy funkcja NIE ma kontekstu użytkownika

Zasada: Webhooki nie mają kontekstu użytkownika,
więc potrzebują service_role.
Ale: Wykonujemy tylko minimalne niezbędne operacje.

Sprawdzalny warunek

# Gdzie używany jest service_role / Admin Client?
grep -rn "SERVICE_ROLE\|createAdminClient\|service_role" \
  volumes/functions/ --include="*.ts" | grep -v "_shared/"

# Oczekiwanie: tylko w funkcjach webhookowych, nie w funkcjach skierowanych do użytkownika

# Czy Admin Client otrzymuje niezweryfikowane dane użytkownika?
# Ręczny przegląd: w każdej funkcji używającej createAdminClient
# sprawdzić czy dane wejściowe są walidowane przed operacją na bazie danych

Scenariusz awarii

Jeśli Edge Function działa z service_role i przekazuje dane wejściowe użytkownika bezpośrednio do zapytań, całkowicie omija RLS. Zmanipulowany payload webhooka mógłby wtedy odczytywać lub zapisywać dowolne dane w dowolnych tabelach, ponieważ klucz service_role nie zna żadnych ograniczeń.

B3 - Walidacja danych wejściowych

Implementacja

Każda Edge Function musi walidować przychodzące dane przed ich przetworzeniem. W Deno Zod działa przez import npm:

// volumes/functions/stripe-webhook/index.ts (fragment)

import { z } from 'npm:zod@3'

// Schemat dla oczekiwanego zdarzenia Stripe
const stripeEventSchema = z.object({
  id: z.string().startsWith('evt_'),
  type: z.string(),
  data: z.object({
    object: z.record(z.unknown()),
  }),
})

// W głównej funkcji po weryfikacji sygnatury:
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' } }
  )
}

// Od tego momentu pracujemy z parsed.data (typowanie bezpieczne)
const event = parsed.data

Sprawdzalny warunek

# Każda funkcja powinna mieć walidację danych wejściowych
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 ma walidację schematu"
  elif grep -qE "JSON\.parse" "$dir/index.ts" 2>/dev/null; then
    echo "OSTRZEŻENIE: $name parsuje JSON bez walidacji schematu"
  fi
done

Scenariusz awarii

Bez walidacji danych wejściowych funkcja akceptuje każdy payload z prawidłową sygnaturą. Skompromitowany klucz API u dostawcy webhooków mógłby wtedy wysyłać nieoczekiwane struktury danych, które prowadzą do niezdefiniowanych operacji bazodanowych, np. undefined jako ID użytkownika w operacji insert.

B4 - Sekrety w zmiennych środowiskowych, nigdy w kodzie

Implementacja

W setup self-hosted sekrety przekazywane są do Edge Functions przez konfigurację Docker Compose:

# docker-compose.yml (fragment)
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    # dodatkowe sekrety dla Functions
# .env.functions (tylko na serwerze, nie w Git)
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_SECRET_KEY=sk_live_...
GITHUB_WEBHOOK_SECRET=ghsec_...
TRIGGER_DEV_API_KEY=tr_...

Dostęp w kodzie:

// Tak: zmienna środowiskowa
const secret = Deno.env.get('STRIPE_WEBHOOK_SECRET')

// NIGDY tak: na stałe w kodzie
const secret = 'whsec_abc123...'

Ogólne podstawy zarządzania sekretami opisano w artykule Bezpieczeństwo danych w infrastrukturze AI dla przedsiębiorstw.

Sprawdzalny warunek

# Sekrety na stałe w kodzie?
grep -rn "sk_live\|sk_test\|whsec_\|ghsec_\|Bearer ey" \
  volumes/functions/ --include="*.ts"
# Oczekiwanie: brak wyników

# .env.functions nie jest w Git?
git ls-files .env.functions
# Oczekiwanie: pusty wynik

# Uprawnienia pliku poprawne?
stat -c "%a" .env.functions
# Oczekiwanie: 600

Scenariusz awarii

Sekrety na stałe w kodzie źródłowym trafiają do repozytorium Git. Nawet jeśli repozytorium jest prywatne, wszyscy deweloperzy z dostępem do repo mają również dostęp do sekretów produkcyjnych. Przy przypadkowym upublicznieniu repozytorium sekrety są natychmiast skompromitowane.

B5 - Unikanie timeoutów i zadań długotrwałych

Implementacja

Edge Functions są zaprojektowane do krótkich, idempotentnych operacji. Self-hosted Edge Runtime ma domyślny timeout (konfigurowalny, typowo 60 sekund dla self-hosted). Zadania długotrwałe blokują sloty workerów.

Odpowiednie (< 30 sekund):
  Odbiór webhooka i aktualizacja bazy danych
  Weryfikacja sygnatury i przekazanie zdarzenia
  Krótkie wywołania API do usług zewnętrznych

NIEODPOWIEDNIE (użyj Trigger.dev):
  Generowanie PDF
  Inferecja AI (wywołania LLM)
  Przetwarzanie wideo/obrazów
  Masowe operacje bazodanowe
  Wysyłka e-mail do wielu odbiorców

Wzorzec dla zadań długotrwałych: Edge Function jako trigger, zadanie w Trigger.dev:

// volumes/functions/process-document/index.ts

// POPRAWNIE: Edge Function tylko triggeruje zadanie
Deno.serve(async (req) => {
  // ... weryfikacja sygnatury, walidacja danych ...

  // Delegowanie zadania długotrwałego do Trigger.dev
  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 },
      }),
    }
  )

  // Natychmiastowa odpowiedź, zadanie działa w tle
  return new Response(
    JSON.stringify({ queued: true }),
    { status: 202, headers: { 'Content-Type': 'application/json' } }
  )
})

Sprawdzalny warunek

# Szukanie wzorców wskazujących na zadania długotrwałe
grep -rn "await.*fetch.*openai\|pdf\|sharp\|ffmpeg\|sleep\|setTimeout" \
  volumes/functions/ --include="*.ts" | grep -v "trigger"
# Oczekiwanie: brak wyników (z wyjątkiem krótkich wywołań API)

# Czy w kodzie są timeouty?
grep -rn "AbortSignal.timeout\|setTimeout" \
  volumes/functions/ --include="*.ts"
# Każde zewnętrzne wywołanie fetch powinno mieć timeout

Scenariusz awarii

Edge Function czekająca 2 minuty na odpowiedź AI API blokuje slot workera w kontenerze Edge Runtime. Przy wielu jednoczesnych żądaniach workery się zapełniają, a kolejne wywołania webhooków (np. od Stripe) kończą się timeoutem. Stripe interpretuje to jako błąd i ponawia, co pogarsza sytuację.

B6 - Obsługa błędów i bezpieczne odpowiedzi

Implementacja

Edge Functions nie mogą ujawniać wewnętrznych szczegółów w odpowiedziach na błędy.

// Wzorzec bezpiecznej obsługi błędów

Deno.serve(async (req) => {
  const corsHeaders = getCorsHeaders(req)

  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    // ... właściwa logika ...

    return new Response(
      JSON.stringify({ success: true }),
      { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )

  } catch (error) {
    // Logowanie wewnętrzne (szczegóły)
    console.error(`Function error: ${error.message}`, {
      stack: error.stack,
      // NIE logujemy sekretów ani danych użytkownika
    })

    // Odpowiedź zewnętrzna (ogólna)
    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }
})

Czego NIE powinno być w odpowiedziach ani logach:

// ŹLE: Stack trace wysyłany do klienta
return new Response(JSON.stringify({ error: error.stack }), { status: 500 })

// ŹLE: Logowanie sekretów
console.log(`Connecting with key: ${Deno.env.get('SERVICE_ROLE_KEY')}`)

// ŹLE: Logowanie pełnych danych użytkownika
console.log(`Processing user: ${JSON.stringify(user)}`)

Sprawdzalny warunek

# Czy szczegóły błędów są wysyłane do klienta?
grep -rn "error\.stack\|error\.message" volumes/functions/ --include="*.ts" | \
  grep "Response"
# Oczekiwanie: brak wyników (stack/message tylko w console.error)

# Czy sekrety są logowane?
grep -rn "console\.log.*KEY\|console\.log.*SECRET\|console\.log.*token" \
  volumes/functions/ --include="*.ts"
# Oczekiwanie: brak wyników

Scenariusz awarii

Jeśli stack trace błędu zostanie wysłany do klienta, atakujący widzi wewnętrzne ścieżki, nazwy modułów i szczegóły połączenia z bazą danych. To znacznie ułatwia ukierunkowane ataki. Jeśli sekrety trafiają do logów, są widoczne dla każdego z dostępem do monitoringu.

Część C - Eksploatacja i monitoring

C1 - Workflow deploymentu

Implementacja

W setup self-hosted nie ma polecenia supabase functions deploy. Workflow jest oparty na plikach:

#!/bin/bash
# scripts/deploy-functions.sh

set -euo pipefail

FUNCTIONS_DIR="/opt/supabase/volumes/functions"
SOURCE_DIR="./supabase/functions"

echo "Deploying Edge Functions..."

# 1. Sprawdzenie składni (Deno)
for file in $(find "$SOURCE_DIR" -name "*.ts" -not -path "*/_shared/*"); do
  deno check "$file" 2>/dev/null || {
    echo "BŁĄD: Błąd składni w $file"
    exit 1
  }
done

# 2. Kopiowanie funkcji
rsync -av --delete \
  --exclude='*.test.ts' \
  "$SOURCE_DIR/" "$FUNCTIONS_DIR/"

# 3. Restart kontenera
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 "Deployment Edge Functions zakończony pomyślnie"
else
  echo "OSTRZEŻENIE: Status Health Check $HEALTH"
fi

Sprawdzalny warunek

# Kontener Edge Runtime działa?
docker compose ps functions --format '{{.State}}'
# Oczekiwanie: running

# Funkcje są osiągalne?
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer ${ANON_KEY}" \
  http://localhost:8000/functions/v1/stripe-webhook
# Oczekiwanie: 200 lub 405 (Method Not Allowed, ponieważ GET zamiast POST)

C2 - Integracja z Claude Code

Claude Code weryfikuje Edge Functions kontekstowo jako uzupełnienie kontroli deterministycznych.

Architektura

Git Push / PR
   |
   +-- Kontrole deterministyczne (CI/CD)
   |   +-- grep po hardcoded secrets
   |   +-- grep po brakującej weryfikacji sygnatury
   |   +-- grep po brakującej walidacji danych
   |   +-- grep po szczegółach błędów w odpowiedziach
   |   +-- Deno Type Check
   |
   +-- Analiza Claude Code (cotygodniowo lub przy PR)
       +-- Nowe funkcje bez weryfikacji sygnatury?
       +-- Użycie service_role uzasadnione?
       +-- Wykryte wzorce zadań długotrwałych?
       +-- Dryft architektoniczny (logika biznesowa w Functions)?
       +-- CORS poprawny dla nowych funkcji?

Konkretny skrypt CI

#!/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+="KRYTYCZNE: Znaleziono hardcoded secrets:\n$SECRETS\n\n"
fi

# 2. Funkcje webhookowe bez weryfikacji sygnatury
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+="KRYTYCZNE: $name nie ma weryfikacji sygnatury\n"
  fi
done

# 3. Funkcje bez walidacji danych
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+="OSTRZEŻENIE: $name parsuje JSON bez walidacji schematu\n"
  fi
done

# 4. Szczegóły błędów w odpowiedziach
LEAKS=$(grep -rn "error\.stack\|error\.message" "$FUNCTIONS_DIR" --include="*.ts" | \
  grep "Response" 2>/dev/null)
if [ -n "$LEAKS" ]; then
  REPORT+="OSTRZEŻENIE: Szczegóły błędów w odpowiedziach:\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+="OSTRZEŻENIE: Znaleziono CORS Wildcard:\n$WILDCARDS\n\n"
fi

# 6. Wzorce zadań długotrwałych
LONG=$(grep -rn "openai\|sharp\|ffmpeg\|puppeteer" "$FUNCTIONS_DIR" --include="*.ts" 2>/dev/null)
if [ -n "$LONG" ]; then
  REPORT+="OSTRZEŻENIE: Możliwe wzorce zadań długotrwałych:\n$LONG\n\n"
fi

# Wynik
if [ -n "$REPORT" ]; then
  echo -e "=== Edge Function Security Check ===\n"
  echo -e "$REPORT"
else
  echo "Wszystkie kontrole Edge Function przeszły pomyślnie."
fi

Claude nie wykonuje żadnych automatycznych zmian na produkcji.

Lista kontrolna deploymentu

Przed każdym deploymentem Edge Functions sprawdź:

Architektura
  [ ] Funkcja to integracja (Webhook/Event), nie logika biznesowa
  [ ] Jeden dostawca webhooków na funkcję
  [ ] Zadania długotrwałe delegowane do Trigger.dev

Bezpieczeństwo
  [ ] Sygnatura webhooka jest weryfikowana
  [ ] Dane wejściowe walidowane schematem (np. Zod)
  [ ] Poprawny wybór klienta (anon vs. service_role)
  [ ] service_role tylko gdy brak kontekstu użytkownika

Sekrety
  [ ] Brak hardcoded secrets w kodzie
  [ ] Sekrety w .env.functions (nie w Git)
  [ ] .env.functions z uprawnieniami 600

CORS
  [ ] Handler OPTIONS obecny
  [ ] Brak wildcard origin w produkcji
  [ ] Nagłówki CORS we WSZYSTKICH odpowiedziach (także błędach)

Obsługa błędów
  [ ] Try/Catch wokół całej logiki
  [ ] Ogólne komunikaty błędów do klienta
  [ ] Szczegóły tylko w console.error (bez sekretów)

Deployment
  [ ] Deno Type Check przeszedł
  [ ] Kontener zrestartowany po deploymencie
  [ ] Health Check po deploymencie pomyślny

Podsumowanie

Supabase Edge Functions stanowią wydajny punkt integracyjny, gdy wykorzystywane są jako taki: odbiór webhooków, weryfikacja sygnatur, przetwarzanie zdarzeń i ich przekazywanie.

Kluczowym aspektem jest rozgraniczenie. Edge Functions nie są drugim backendem obok Next.js ani runnerem zadań obok Trigger.dev. Kto wyraźnie wytycza tę granicę i wdraża podstawy bezpieczeństwa (sygnatury, walidacja danych, poprawny klient, CORS), może bezpiecznie eksploatować Edge Functions w setup self-hosted.

Połączenie kontroli deterministycznych w CI i kontekstowej analizy Claude Code pokrywa zarówno znane wzorce, jak i nowe, nieoczekiwane zagrożenia. Kto od początku stawia na Cert-Ready-by-Design, oszczędza sobie dodatkowych rund audytowych.

Pobierz listę kontrolną audytu

Przygotowany prompt dla Claude Code. Prześlij plik na swój serwer i uruchom Claude Code w katalogu projektu Edge Functions. Claude Code automatycznie sprawdzi wszystkie punkty bezpieczeństwa z tego runbooka i zgłosi ZALICZONY, OSTRZEŻENIE lub KRYTYCZNY.

claude -p "$(cat claude-check-artikel-3-edge-functions-pl.md)" --allowedTools Read,Grep,Glob,Bash

Pobierz listę kontrolną

Spis treści serii

Ten artykuł jest częścią naszej serii DevOps dla self-hosted stacków aplikacyjnych.

  1. Supabase Self-Hosting Runbook
  2. Next.js nad Supabase - bezpieczna eksploatacja
  3. Supabase Edge Functions - bezpieczne wdrożenie ← ten artykuł
  4. Bezpieczna obsługa Trigger.dev Background Jobs
  5. Claude Code jako kontrola bezpieczeństwa w DevOps
  6. Security Baseline dla całego stacku

W kolejnym artykule pokażemy, jak bezpiecznie eksploatować Trigger.dev Background Jobs, bez wprowadzania nowych zagrożeń bezpieczeństwa w stacku.

Bert Gogolin

Bert Gogolin

Dyrektor Generalny, Gosign

AI Governance Briefing

Enterprise AI, regulacje i infrastruktura - raz w miesiącu, bezpośrednio ode mnie.

Bez spamu. Możliwość rezygnacji w każdej chwili. Polityka prywatności

Supabase Edge Functions Deno Webhooks Security
Udostępnij artykuł

Najczęściej zadawane pytania

Do czego nadają się Supabase Edge Functions?

Edge Functions nadają się do obsługi webhooków, integracji z zewnętrznymi API i sygnowanych endpointów zdarzeń. Nie stanowią alternatywy dla Next.js Server Actions w zakresie logiki biznesowej ani zamiennika Trigger.dev dla zadań długotrwałych.

Dlaczego każdy endpoint webhookowy musi weryfikować sygnaturę?

Bez weryfikacji sygnatury dowolna osoba może wysyłać dowolne payloady do webhooka. Atakujący mógłby wyzwalać sfałszowane zdarzenia, na przykład fałszywe potwierdzenie płatności Stripe, mimo że faktycznie nie doszło do zapłaty.

Dlaczego webhooki potrzebują weryfikacji sygnatury, ale nie JWT?

Webhooki są wywoływane przez usługi zewnętrzne takie jak Stripe czy GitHub, które nie posiadają JWT Supabase. Zamiast tego podpisują payload wspólnym sekretem (HMAC). Edge Function musi zweryfikować tę sygnaturę, ponieważ w przeciwnym razie każdy może wysłać dowolny payload.