Supabase Edge Functions - bezpieczne wdrożenie
Runbook DevOps dla Supabase Edge Functions: webhooki, sygnatury, CORS, Deno runtime, walidacja danych i integracja z Claude Code.
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-runtimei 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.
- Supabase Self-Hosting Runbook
- Next.js nad Supabase - bezpieczna eksploatacja
- Supabase Edge Functions - bezpieczne wdrożenie ← ten artykuł
- Bezpieczna obsługa Trigger.dev Background Jobs
- Claude Code jako kontrola bezpieczeństwa w DevOps
- 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
| Kryterium | Edge Function | Server Action | Trigger.dev Task |
|---|---|---|---|
| Źródło | Zewnętrzne zdarzenie (webhook) | Dane wejściowe użytkownika | Zadanie wewnętrzne |
| Czas wykonania | < 30 sekund | < 10 sekund | Do 5 minut+ |
| Uwierzytelnianie | Sygnatura HMAC | Sesja użytkownika (JWT) | Klucz API / service_role |
| RLS | Omijany (service_role) | Aktywny (anon key) | Omijany (service_role) |
| Retry | Dostawca webhook | Brak (synchronicznie) | Wbudowany (Trigger.dev) |
| Przykład | Stripe checkout webhook | Formularz profilu | Generowanie 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.
- Supabase Self-Hosting Runbook
- Next.js nad Supabase - bezpieczna eksploatacja
- Supabase Edge Functions - bezpieczne wdrożenie ← ten artykuł
- Bezpieczna obsługa Trigger.dev Background Jobs
- Claude Code jako kontrola bezpieczeństwa w DevOps
- 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
Dyrektor Generalny, Gosign
AI Governance Briefing
Enterprise AI, regulacje i infrastruktura - raz w miesiącu, bezpośrednio ode mnie.