Trigger.dev Background Jobs com segurança
Runbook DevOps para Trigger.dev v3: setup Self-Hosted, definição de tasks, idempotência, concurrency, secrets e integração com Claude Code.
Assim que uma aplicação vai além de operações CRUD simples, surgem tarefas que não devem rodar de forma síncrona no ciclo request-response: envio de e-mails, processamento de webhooks, jobs de importação/exportação, tarefas de IA, geração de PDF, migração de dados e tarefas periódicas.
Essas tarefas não pertencem a Next.js Server Actions (bloqueiam o servidor web), não pertencem a Supabase Edge Functions (limite de timeout, sem processos de longa duração) e também não pertencem a Cron Jobs no servidor (sem lógica de retry, sem monitoramento).
Elas pertencem a uma camada de jobs dedicada. No nosso stack, o Trigger.dev assume esse papel.
Este runbook descreve como operar o Trigger.dev com segurança no stack. Cada passo contém uma implementação concreta com a API atual do Trigger.dev v3, uma condição verificável e um cenário de falha.
Nota sobre a arquitetura: O Trigger.dev consiste em duas partes: a Platform (Webapp, Dashboard, Queue Management) e o Worker (executa as suas tasks). Operamos ambos como self-hosted na nossa própria infraestrutura. Este runbook descreve exclusivamente o setup Self-Hosted com Trigger.dev v3.
Resumo - Artigo 4 de 6 da série DevOps Runbook
- Trigger.dev como stack Docker separado com seu próprio PostgreSQL
- Cada task precisa de maxDuration, concurrencyLimit e configuração de retry
- Chaves de idempotência em chamadas de API externas
- Acesso ao banco de dados limitado aos campos necessários
- Logger do Trigger.dev em vez de console.log
Sumário da série
Este guia faz parte da nossa série de runbooks DevOps para stacks de aplicações self-hosted.
- Supabase Self-Hosting Runbook
- Next.js sobre Supabase com segurança
- Supabase Edge Functions com segurança
- Trigger.dev Background Jobs com segurança - este artigo
- Claude Code como controle de segurança no workflow DevOps
- Security Baseline para todo o stack
O artigo 1 descreve a plataforma. O artigo 2 descreve a camada de aplicação. O artigo 3 descreve integrações. Este artigo descreve processamento assíncrono de jobs.
Visão geral da arquitetura
Browser
|
Next.js (camada de aplicação)
|
+-- tasks.trigger("send-email", payload) <- iniciar job
|
Trigger.dev Platform
|
+-- Queue Management
+-- Retry Logic
+-- Dashboard / Monitoring
|
Trigger.dev Worker
|
+-- send-email.ts -> SMTP API
+-- generate-report.ts -> PDF + Supabase Storage
+-- process-webhook.ts -> Supabase DB (service_role)
+-- sync-crm.ts -> externe CRM API
|
+-- Supabase (via service_role ou conexão direta com o DB)
|
PostgreSQL
Regras fundamentais:
User Request -> Next.js Server Action / Route Handler
Evento curto -> Supabase Edge Function
Longa duração -> Trigger.dev Task
Periódico -> Trigger.dev Scheduled Task
Guia de configuração de tasks
| Tipo de task | maxDuration | concurrencyLimit | Estratégia de Retry |
|---|---|---|---|
| Envio de email | 30s | 5-10 | 3 tentativas, exponential backoff |
| Geração de PDF | 120s | 2-3 | 2 tentativas, intervalo fixo |
| Inferência AI (LLM) | 300s | 1-3 | 2 tentativas, exponential backoff |
| Migração de dados | 600s | 1 | 1 tentativa, tratamento manual |
| Processamento de webhooks | 30s | 10 | 3 tentativas, exponential backoff |
Ponto crítico de segurança
Trigger.dev Tasks tipicamente têm acesso total ao banco de dados sem RLS. Eles se conectam via service_role Key (pelo Supabase Client) ou via conexão PostgreSQL direta (pelo DATABASE_URL). Em ambos os casos, as Row Level Security Policies não se aplicam. Essa é a diferença mais importante em relação a requests que passam pelo PostgREST com a chave anon.
Quem conhece os fundamentos de gerenciamento de secrets entende por que essa separação é essencial.
Parte A - Decisões de arquitetura
A1 - Operar Trigger.dev v3 self-hosted como serviço próprio
Implementação
O Trigger.dev roda separado do Next.js e do Supabase como seu próprio Docker Stack. A arquitetura Self-Hosted v3 consiste em três componentes:
Next.js App (seu servidor Locaweb / Magalu Cloud)
|
+-- Trigger.dev Platform (Webapp + Queue + Dashboard)
|
+-- Trigger.dev Worker (executa suas tasks)
|
+-- Trigger.dev PostgreSQL (instância própria de DB)
# docker-compose.trigger.yml
services:
trigger-platform:
image: ghcr.io/triggerdotdev/trigger.dev:v3 # Version pinnen
restart: unless-stopped
ports:
- "127.0.0.1:3040:3040" # NUR localhost, Reverse Proxy davor
environment:
DATABASE_URL: postgresql://postgres:${TRIGGER_DB_PASSWORD}@trigger-db:5433/trigger
DIRECT_URL: postgresql://postgres:${TRIGGER_DB_PASSWORD}@trigger-db:5433/trigger
LOGIN_ORIGIN: https://trigger.example.com
APP_ORIGIN: https://trigger.example.com
MAGIC_LINK_SECRET: ${MAGIC_LINK_SECRET}
SESSION_SECRET: ${SESSION_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
RUNTIME_PLATFORM: docker-compose
# Worker-Konfiguration
DOCKER_SOCKET_LOCATION: /var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Worker-Management
depends_on:
trigger-db:
condition: service_healthy
networks:
- trigger-internal
trigger-db:
image: postgres:15
restart: unless-stopped
ports:
- "127.0.0.1:5433:5432" # Port 5433, NICHT 5432 (Supabase nutzt 5432)
environment:
POSTGRES_PASSWORD: ${TRIGGER_DB_PASSWORD}
POSTGRES_DB: trigger
volumes:
- trigger-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- trigger-internal
volumes:
trigger-db-data:
networks:
trigger-internal:
driver: bridge
Importante: O Trigger.dev precisa da sua própria instância PostgreSQL na porta 5433. Não compartilhe o banco de dados do Supabase (porta 5432). O Trigger.dev armazena status de fila, histórico de execuções, estado dos workers e metadados no seu banco. São dados diferentes dos dados da sua aplicação.
Configurar o SDK no projeto Next.js:
# Trigger.dev SDK installieren
npm install @trigger.dev/sdk@3
# .env (Next.js)
TRIGGER_SECRET_KEY=tr_dev_... # aus dem self-hosted Dashboard
TRIGGER_API_URL=http://localhost:3040 # lokale Platform URL
Worker Deployment:
No setup self-hosted v3, o deploy das tasks é feito pela CLI, que se comunica com a sua Platform local:
# Tasks deployen (zeigt auf eure self-hosted Platform)
npx trigger.dev@latest deploy --self-hosted
# Lokale Entwicklung
npx trigger.dev@latest dev
Nota: O Worker Docker v3 utiliza Docker-in-Docker (Socket Mounting). Isso significa que a Trigger.dev Platform precisa de acesso ao Docker Socket (
/var/run/docker.sock). Isso tem implicações de segurança: um container Trigger.dev comprometido poderia iniciar containers Docker arbitrários. Por isso, o Trigger.dev deveria idealmente rodar em um servidor próprio ou em uma rede Docker isolada.
Condição verificável
# Container laufen?
docker compose -f docker-compose.trigger.yml ps
# Erwartung: trigger-platform und trigger-db running
# Platform erreichbar (intern)?
curl -s -o /dev/null -w "%{http_code}" http://localhost:3040
# Erwartung: 200 oder 302 (Redirect zu Login)
# Platform von außen NICHT direkt erreichbar?
curl -s -o /dev/null -w "%{http_code}" https://trigger.example.com:3040
# Erwartung: Connection refused (nur über Reverse Proxy)
# Trigger.dev DB auf eigenem Port?
ss -tlnp | grep 5433
# Erwartung: Listening auf 127.0.0.1:5433
# Supabase DB auf anderem Port?
ss -tlnp | grep 5432
# Erwartung: Listening auf 10.0.1.10:5432 (internes Interface)
# Docker Socket gemountet?
docker compose -f docker-compose.trigger.yml exec trigger-platform ls -la /var/run/docker.sock
# Erwartung: Socket vorhanden
Cenário de falha
Quando o Trigger.dev roda no mesmo processo que o Next.js, tasks de longa duração bloqueiam o servidor web. Uma tarefa de IA que leva 5 minutos ocupa um worker do Next.js. Com poucos worker threads (padrão: 1 por CPU), toda a aplicação se torna irresponsiva para os usuários. Se o Trigger.dev compartilha o banco do Supabase, queries da fila de jobs competem com requests de usuários pelas conexões do banco. Se o Docker Socket é montado sem isolamento de rede, uma task comprometida pode iniciar containers que acessam a rede do host.
A2 - Controlar acesso ao banco de dados a partir das tasks
Implementação
As tasks precisam de acesso aos dados da sua aplicação no Supabase. Existem dois caminhos:
Caminho 1: Supabase Client com service_role (recomendado)
// trigger/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export function createTaskClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
}
Vantagem: passa pelo PostgREST, utiliza a camada de API do Supabase. Desvantagem: ignora o RLS (service_role).
Caminho 2: Conexão direta com o DB (para queries complexas)
// trigger/lib/db.ts
import postgres from 'postgres'
const sql = postgres(process.env.DATABASE_URL!, {
max: 5, // Connection Pool limitieren
idle_timeout: 20,
connect_timeout: 10,
})
export { sql }
Desvantagem: ignora tanto o RLS quanto o PostgREST. Acesso total ao banco.
Regra: Independentemente do caminho escolhido, a task tem mais permissões do que um request normal de usuário. Por isso, cada task deve definir seu próprio escopo de forma clara.
// RICHTIG: Task greift nur auf das zu, was er braucht
const { data } = await supabase
.from('invoices')
.select('id, amount, user_id')
.eq('id', payload.invoiceId)
.single()
// FALSCH: Task liest alle Daten einer Tabelle
const { data } = await supabase
.from('invoices')
.select('*')
Quem conhece os patterns de RLS do runbook de Supabase Self-Hosting entende por que a diferença de escopo é crítica.
Condição verificável
# Welche Tasks nutzen service_role oder DATABASE_URL?
grep -rn "SERVICE_ROLE\|DATABASE_URL\|createTaskClient\|postgres(" \
trigger/ --include="*.ts"
# Für jeden Treffer manuell prüfen:
# - Greift der Task nur auf die Daten zu, die er braucht?
# - Ist der Input validiert bevor er in die Query fließt?
# - Gibt es einen Grund für direkten DB-Zugriff statt Supabase Client?
Cenário de falha
Uma task com acesso direto ao banco e input não validado pode levar a SQL Injection. Uma task que faz SELECT * em tabelas grandes pode comprometer a performance do banco para todos os usuários. Um connection pool sem controle (sem limite max) pode ocupar todas as conexões PostgreSQL disponíveis e paralisar toda a aplicação.
Parte B - Verificações de implementação
Estas regras valem para cada task e devem ser conferidas a cada deployment.
B1 - Definir tasks corretamente (API v3)
Implementação
O Trigger.dev v3 utiliza a função task() do @trigger.dev/sdk/v3. Cada task é exportada e tem um ID único.
// trigger/tasks/send-welcome-email.ts
import { task } from '@trigger.dev/sdk/v3'
import { z } from 'zod'
import { createTaskClient } from '../lib/supabase'
// 1. Payload Schema definieren
const payloadSchema = z.object({
userId: z.string().uuid(),
email: z.string().email(),
})
export const sendWelcomeEmail = task({
id: 'send-welcome-email',
// 2. Retry-Strategie
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 1_000,
maxTimeoutInMs: 30_000,
},
// 3. Concurrency begrenzen
queue: {
concurrencyLimit: 5,
},
// 4. Maximum Laufzeit
maxDuration: 60, // 60 Sekunden
// 5. Run-Funktion
run: async (payload) => {
// Input validieren
const parsed = payloadSchema.safeParse(payload)
if (!parsed.success) {
throw new Error(`Invalid payload: ${parsed.error.message}`)
}
const { userId, email } = parsed.data
const supabase = createTaskClient()
// User-Daten holen (nur was nötig ist)
const { data: user } = await supabase
.from('profiles')
.select('full_name, locale')
.eq('id', userId)
.single()
if (!user) {
throw new Error(`User not found: ${userId}`)
}
// E-Mail senden
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{ to: [{ email }] }],
from: { email: 'hello@example.com' },
subject: `Willkommen, ${user.full_name}!`,
content: [{ type: 'text/plain', value: `...` }],
}),
})
if (!response.ok) {
// Fehler werfen, damit Retry greift
throw new Error(`SendGrid error: ${response.status}`)
}
// Status in DB aktualisieren
await supabase
.from('profiles')
.update({ welcome_email_sent: true })
.eq('id', userId)
return { success: true, userId }
},
})
Disparar task a partir do Next.js
// app/actions/auth.ts
'use server'
import { tasks } from '@trigger.dev/sdk/v3'
import type { sendWelcomeEmail } from '@/trigger/tasks/send-welcome-email'
export async function onUserSignup(userId: string, email: string) {
// Task triggern (kehrt sofort zurück)
const handle = await tasks.trigger<typeof sendWelcomeEmail>(
'send-welcome-email',
{ userId, email }
)
// handle.id enthält die Run-ID für Tracking
return { triggered: true, runId: handle.id }
}
Condição verificável
# Alle Tasks müssen exportiert sein
for file in trigger/tasks/*.ts; do
if ! grep -q "export const" "$file"; then
echo "WARNUNG: $file hat keinen exportierten Task"
fi
done
# Alle Tasks müssen eine ID haben
grep -rn "id:" trigger/tasks/ --include="*.ts" | grep "task({" -A1
# Tasks dürfen NICHT aus Client-Code getriggert werden
grep -rn "tasks.trigger\|\.trigger(" app/ --include="*.tsx" | grep -v "use server"
# Erwartung: keine Treffer (nur in Server-Kontext)
Cenário de falha
Uma task sem export não é reconhecida pelo Trigger.dev e desaparece no deployment sem mensagem de erro. Uma task sem configuração de retry usa o padrão (sem retry), de modo que um erro temporário de API (por exemplo, timeout do SendGrid) resulta na perda permanente do e-mail.
B2 - Garantir idempotência
Implementação
O Trigger.dev possui um sistema de idempotência integrado via idempotencyKey. Isso é preferível ao check findUnique feito manualmente.
Ao disparar (evita acionamento duplicado):
// Wenn derselbe User zweimal schnell "Signup" klickt,
// wird der Task nur einmal ausgeführt
await tasks.trigger<typeof sendWelcomeEmail>(
'send-welcome-email',
{ userId, email },
{
idempotencyKey: `welcome-email-${userId}`,
idempotencyKeyTTL: '24h', // Key gilt 24 Stunden
}
)
Dentro da task (evita efeitos colaterais duplicados em retries):
import { task, idempotencyKeys } from '@trigger.dev/sdk/v3'
export const processPayment = task({
id: 'process-payment',
retry: { maxAttempts: 3 },
run: async (payload: { orderId: string; amount: number }) => {
// Idempotency Key für den Stripe-Call
// Bei Retry wird der Stripe-Call NICHT wiederholt
const stripeKey = await idempotencyKeys.create(
`stripe-charge-${payload.orderId}`
)
const charge = await stripe.charges.create(
{
amount: payload.amount,
currency: 'eur',
source: 'tok_...',
},
{ idempotencyKey: stripeKey }
)
// DB-Update ist idempotent (upsert statt insert)
await supabase
.from('payments')
.upsert({
order_id: payload.orderId,
stripe_charge_id: charge.id,
status: 'completed',
}, { onConflict: 'order_id' })
return { chargeId: charge.id }
},
})
Condição verificável
# Tasks mit externen API-Calls sollten Idempotency Keys haben
for file in trigger/tasks/*.ts; do
if grep -qE "fetch\(|stripe\.|sendgrid\.|resend\." "$file"; then
if ! grep -qE "idempotencyKey|idempotencyKeys" "$file"; then
echo "WARNUNG: $file hat externe API-Calls ohne Idempotency Key"
fi
fi
done
Cenário de falha
Sem proteção de idempotência ao disparar, um webhook duplicado (Stripe faz retry em caso de timeout) pode acionar a mesma task duas vezes. Sem idempotência dentro da task, um retry após um erro parcial pode causar pagamentos duplicados ou e-mails duplicados.
B3 - Configurar timeouts e concurrency corretamente
Implementação
O Trigger.dev v3 possui maxDuration (limite de tempo de execução por task) e concurrencyLimit (execuções paralelas). Ambos devem ser definidos conscientemente para cada task.
import { task, queue } from '@trigger.dev/sdk/v3'
// Shared Queue für E-Mail-Tasks (verhindert SMTP Rate Limiting)
const emailQueue = queue({
name: 'email-processing',
concurrencyLimit: 5, // max. 5 parallele E-Mail-Tasks
})
export const sendEmail = task({
id: 'send-email',
queue: emailQueue,
maxDuration: 30, // 30 Sekunden max
run: async (payload) => {
// ...
},
})
// AI-Tasks brauchen mehr Zeit, aber weniger Parallelität
export const analyzeDocument = task({
id: 'analyze-document',
queue: { concurrencyLimit: 2 }, // max. 2 parallele AI-Calls
maxDuration: 300, // 5 Minuten max
run: async (payload) => {
// ...
},
})
// Kritische Tasks die sequentiell laufen müssen
export const processInvoice = task({
id: 'process-invoice',
queue: { concurrencyLimit: 1 }, // strikt sequentiell
maxDuration: 60,
run: async (payload) => {
// ...
},
})
Valores de referência:
E-Mail senden: maxDuration 30s, concurrency 5-10
PDF generieren: maxDuration 120s, concurrency 2-3
AI-Inference (LLM): maxDuration 300s, concurrency 1-3
Datenbank-Migration: maxDuration 600s, concurrency 1
Webhook verarbeiten: maxDuration 30s, concurrency 10
Condição verificável
# Alle Tasks müssen maxDuration haben
for file in trigger/tasks/*.ts; do
if ! grep -q "maxDuration" "$file"; then
echo "WARNUNG: $file hat kein maxDuration"
fi
done
# Alle Tasks müssen concurrencyLimit haben (oder eine shared Queue nutzen)
for file in trigger/tasks/*.ts; do
if ! grep -qE "concurrencyLimit|queue:" "$file"; then
echo "WARNUNG: $file hat kein Concurrency Limit"
fi
done
Cenário de falha
Sem maxDuration, uma task pode rodar infinitamente em caso de uma chamada de API travada e bloquear permanentemente um slot de worker. Sem concurrencyLimit, 100 tasks de e-mail disparadas simultaneamente podem sobrecarregar o provedor SMTP e causar rate limiting. Sem concurrency limit em tasks intensivas de banco de dados, todas as conexões PostgreSQL podem ser ocupadas ao mesmo tempo.
Estatística: De acordo com dados do Trigger.dev, mais de 40% das instâncias de produção self-hosted possuem tasks sem maxDuration configurado - levando ao bloqueio silencioso de workers.
B4 - Secrets e variáveis de ambiente
Implementação
Trigger.dev Tasks rodam em um ambiente separado. Os secrets devem ser passados explicitamente pela configuração do Docker Compose.
# .env.trigger (nur auf dem Server, nicht im Git)
TRIGGER_DB_PASSWORD=...
MAGIC_LINK_SECRET=...
SESSION_SECRET=...
ENCRYPTION_KEY=...
# Secrets für eure Tasks (werden an Worker weitergereicht)
SUPABASE_SERVICE_ROLE_KEY=eyJ...
DATABASE_URL=postgresql://...
SENDGRID_API_KEY=SG.xxx
OPENAI_API_KEY=sk-xxx
STRIPE_SECRET_KEY=sk_live_xxx
Os secrets das tasks são passados no setup self-hosted pelo Dashboard do Trigger.dev (Environment Variables) ou pelo ambiente Docker para os containers de worker.
No código:
// RICHTIG: Environment Variable
const apiKey = process.env.SENDGRID_API_KEY
// FALSCH: Hardcoded
const apiKey = 'SG.xxx...'
Condição verificável
# Hardcoded Secrets?
grep -rn "sk_live\|sk_test\|SG\.\|sk-\|Bearer ey" \
trigger/ --include="*.ts"
# Erwartung: keine Treffer
# .env.trigger nicht im Git?
git ls-files .env.trigger
# Erwartung: leer
# Alle benötigten Env Vars gesetzt?
for var in SUPABASE_SERVICE_ROLE_KEY SENDGRID_API_KEY; do
if [ -z "${!var}" ]; then
echo "FEHLT: $var"
fi
done
Cenário de falha
Secrets em arquivos de task acabam no repositório Git e no build artifact (Worker Image). No setup self-hosted, as Worker Images são construídas e armazenadas localmente. Secrets hardcoded nessas imagens ficam visíveis para qualquer pessoa com acesso ao Docker Host ou ao registry.
B5 - Error handling e logging
Implementação
As tasks devem tratar erros de forma limpa e não registrar dados sensíveis nos logs.
import { task, logger } from '@trigger.dev/sdk/v3'
export const processOrder = task({
id: 'process-order',
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 1_000,
maxTimeoutInMs: 30_000,
},
run: async (payload: { orderId: string }) => {
// Trigger.dev Logger (strukturiert, im Dashboard sichtbar)
logger.info('Processing order', { orderId: payload.orderId })
try {
const result = await processOrderInternal(payload.orderId)
logger.info('Order processed', {
orderId: payload.orderId,
status: result.status,
})
return result
} catch (error) {
// Strukturiert loggen (ohne sensible Details)
logger.error('Order processing failed', {
orderId: payload.orderId,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
// NICHT: error.stack, payload Details, User-Daten
})
// Fehler weiterwerfen, damit Retry greift
throw error
}
},
// onFailure Hook: wird nach allen Retries aufgerufen
onFailure: async (payload, error, params) => {
logger.error('Order permanently failed after all retries', {
orderId: payload.orderId,
attemptNumber: params.run.attemptNumber,
})
// Alert senden (z.B. Slack Notification)
await sendAlertToOps({
task: 'process-order',
orderId: payload.orderId,
error: error.message,
})
},
})
O que NÃO deve ser registrado nos logs:
// FALSCH
logger.info('Processing user', { user }) // vollständiges User-Objekt
logger.info('API call', { headers: req.headers }) // Auth-Header mit Token
logger.info('DB query', { connectionString }) // Datenbank-URL
console.log(process.env.SUPABASE_SERVICE_ROLE_KEY) // Secret
Condição verificável
# console.log statt logger?
grep -rn "console\.log\|console\.error" trigger/tasks/ --include="*.ts"
# Erwartung: keine Treffer (immer logger von @trigger.dev/sdk verwenden)
# Sensible Daten in Logs?
grep -rn "logger\.\(info\|error\|warn\)" trigger/ --include="*.ts" | \
grep -iE "password|secret|key|token|email.*:" | \
grep -v "orderId\|taskId\|runId"
# Erwartung: keine Treffer
Cenário de falha
O Trigger.dev armazena todos os logs e os exibe no Dashboard. Se console.log(user) registra um objeto de usuário completo com e-mail e metadados, esses dados ficam visíveis no Dashboard do Trigger.dev para qualquer pessoa com acesso ao Dashboard, mesmo meses depois, já que o histórico de execuções é persistente.
Parte C - Operação e monitoramento
C1 - Monitoramento e alertas
Implementação
O Trigger.dev possui um Dashboard integrado com histórico de execuções, logs e traces. Além disso, estas métricas devem ser monitoradas ativamente:
Kritische Metriken:
- Failed Runs (letzte 24h) -> Alert wenn > 5
- Queue Length -> Alert wenn > 100
- Average Run Duration -> Alert wenn > 2x Baseline
- Runs in WAITING State -> Info, kein sofortiger Alert
Proteger o Dashboard Self-Hosted:
# Das Trigger.dev Dashboard darf NICHT öffentlich erreichbar sein
# Zugang nur über:
# - Reverse Proxy mit Basic Auth
# - VPN / WireGuard
# - SSH Tunnel
# Beispiel: SSH Tunnel zum Dashboard
ssh -L 3040:localhost:3040 deploy@trigger-server
# Dann im Browser: http://localhost:3040
Health check automatizado:
#!/bin/bash
# scripts/check-trigger-health.sh
# Trigger.dev Platform erreichbar?
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3040/healthcheck)
if [ "$HEALTH" != "200" ]; then
echo "KRITISCH: Trigger.dev Platform nicht erreichbar"
fi
# Worker-Container läuft?
WORKER_STATUS=$(docker compose -f docker-compose.trigger.yml ps trigger-worker --format '{{.State}}')
if [ "$WORKER_STATUS" != "running" ]; then
echo "KRITISCH: Trigger.dev Worker nicht running ($WORKER_STATUS)"
fi
# Failed Runs der letzten 24h (über Trigger.dev DB)
FAILED=$(docker compose -f docker-compose.trigger.yml exec -T trigger-db \
psql -U postgres -d trigger -t -c \
"SELECT count(*) FROM task_runs WHERE status = 'FAILED' AND created_at > now() - interval '24 hours';" 2>/dev/null)
if [ "${FAILED:-0}" -gt 5 ]; then
echo "WARNUNG: $FAILED fehlgeschlagene Runs in den letzten 24h"
fi
Condição verificável
# Dashboard nur intern erreichbar?
curl -s -o /dev/null -w "%{http_code}" https://trigger.example.com:3040
# Erwartung: Connection refused oder 401
# Health Check Script läuft täglich?
crontab -l | grep "check-trigger-health"
# Erwartung: Eintrag vorhanden
C2 - Dead Letter Handling
Implementação
Tasks que falham após todos os retries ficam no status FAILED. Elas devem ser tratadas ativamente.
import { task, logger } from '@trigger.dev/sdk/v3'
import { createTaskClient } from '../lib/supabase'
export const criticalTask = task({
id: 'critical-task',
retry: {
maxAttempts: 5,
factor: 2,
minTimeoutInMs: 2_000,
maxTimeoutInMs: 60_000,
},
run: async (payload: { orderId: string }) => {
// ... Task-Logik
},
onFailure: async (payload, error, params) => {
const supabase = createTaskClient()
// Failed Job in eigene Tabelle schreiben
await supabase
.from('failed_jobs')
.insert({
task_id: 'critical-task',
payload: JSON.stringify(payload),
error_message: error.message,
attempts: params.run.attemptNumber,
failed_at: new Date().toISOString(),
resolved: false,
})
// Alert
logger.error('Critical task permanently failed', {
orderId: payload.orderId,
attempts: params.run.attemptNumber,
})
},
})
-- Migration: failed_jobs Tabelle
CREATE TABLE IF NOT EXISTS failed_jobs (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
task_id TEXT NOT NULL,
payload JSONB NOT NULL,
error_message TEXT,
attempts INTEGER,
failed_at TIMESTAMPTZ NOT NULL,
resolved BOOLEAN DEFAULT false,
resolved_at TIMESTAMPTZ,
resolved_by TEXT
);
-- RLS: nur service_role darf zugreifen
ALTER TABLE failed_jobs ENABLE ROW LEVEL SECURITY;
-- Keine Policy für anon/authenticated = kein Zugriff über PostgREST
Condição verificável
# Gibt es unresolvierte Failed Jobs?
# Über Supabase Client oder direkte DB-Query
psql -c "SELECT count(*) FROM failed_jobs WHERE resolved = false;"
# Erwartung: 0 (oder aktiv in Bearbeitung)
# Haben alle kritischen Tasks einen onFailure Hook?
grep -L "onFailure" trigger/tasks/*.ts
# Erwartung: nur unkritische Tasks ohne onFailure
C3 - Integração com Claude Code
Arquitetura
Git Push / PR
|
+-- Deterministische Checks (CI/CD)
| +-- Alle Tasks haben maxDuration?
| +-- Alle Tasks haben concurrencyLimit?
| +-- Externe API-Calls haben Idempotency Keys?
| +-- Keine hardcoded Secrets?
| +-- Kein console.log (nur logger)?
|
+-- Claude Code Analyse (wöchentlich oder bei PR)
+-- Neue Tasks ohne Retry-Strategie?
+-- DB-Zugriff korrekt eingeschränkt?
+-- Idempotenz-Patterns konsistent?
+-- onFailure für kritische Tasks vorhanden?
+-- Concurrency Limits angemessen?
Script CI
#!/bin/bash
# scripts/check-trigger-tasks.sh
REPORT=""
# 1. Tasks ohne maxDuration
for file in trigger/tasks/*.ts; do
name=$(basename "$file" .ts)
if ! grep -q "maxDuration" "$file"; then
REPORT+="WARNUNG: $name hat kein maxDuration\n"
fi
done
# 2. Tasks ohne Concurrency
for file in trigger/tasks/*.ts; do
name=$(basename "$file" .ts)
if ! grep -qE "concurrencyLimit|queue:" "$file"; then
REPORT+="WARNUNG: $name hat kein Concurrency Limit\n"
fi
done
# 3. Externe API-Calls ohne Idempotency
for file in trigger/tasks/*.ts; do
name=$(basename "$file" .ts)
if grep -qE "fetch\(|stripe\.|resend\." "$file"; then
if ! grep -qE "idempotencyKey|idempotencyKeys" "$file"; then
REPORT+="WARNUNG: $name hat externe API-Calls ohne Idempotency\n"
fi
fi
done
# 4. Hardcoded Secrets
SECRETS=$(grep -rn "sk_live\|sk_test\|SG\.\|sk-" trigger/ --include="*.ts" 2>/dev/null)
if [ -n "$SECRETS" ]; then
REPORT+="KRITISCH: Hardcoded Secrets:\n$SECRETS\n\n"
fi
# 5. console.log statt logger
CONSOLE=$(grep -rn "console\.\(log\|error\|warn\)" trigger/tasks/ --include="*.ts" 2>/dev/null)
if [ -n "$CONSOLE" ]; then
REPORT+="WARNUNG: console.log statt Trigger.dev logger:\n$CONSOLE\n\n"
fi
# 6. Nicht exportierte Tasks
for file in trigger/tasks/*.ts; do
name=$(basename "$file" .ts)
if ! grep -q "export const" "$file"; then
REPORT+="KRITISCH: $name Task ist nicht exportiert\n"
fi
done
if [ -n "$REPORT" ]; then
echo -e "=== Trigger.dev Task Security Check ===\n$REPORT"
else
echo "Alle Trigger.dev Checks bestanden."
fi
O Claude não executa alterações automáticas em produção.
Checklist de deployment
Verificar antes de cada deployment de Trigger.dev Tasks:
Arquitetura
[ ] Trigger.dev roda como serviço próprio (não no processo Next.js)
[ ] Instância PostgreSQL própria (não o banco do Supabase)
[ ] Dashboard acessível apenas internamente
Definição de tasks
[ ] Cada task é exportada
[ ] Cada task tem um ID único
[ ] Cada task tem maxDuration
[ ] Cada task tem concurrencyLimit ou shared Queue
[ ] Cada task tem configuração de retry
Idempotência
[ ] Chamadas de trigger têm idempotencyKey quando necessário
[ ] Chamadas de API externas dentro das tasks têm Idempotency
[ ] Operações de banco são idempotentes (upsert em vez de insert quando possível)
Acesso ao banco de dados
[ ] service_role apenas via createTaskClient()
[ ] Queries acessam apenas os dados necessários (sem SELECT *)
[ ] Connection pool limitado (max: 5-10)
Secrets
[ ] Nenhum secret hardcoded no código
[ ] Secrets via Environment Variables / Dashboard
[ ] .env.trigger não está no Git
Logging
[ ] Trigger.dev logger em vez de console.log
[ ] Nenhum dado sensível nos logs (objetos de usuário, tokens, keys)
Error handling
[ ] Tasks críticas possuem hook onFailure
[ ] Failed jobs são armazenados na tabela failed_jobs
[ ] Alertas configurados para falhas permanentes
Conclusão
O Trigger.dev forma a camada de processamento em background no stack. Usado corretamente, ele processa jobs assíncronos de forma confiável, com retries, idempotência e monitoramento.
O ponto de segurança mais crítico é o acesso ao banco de dados: tasks tipicamente têm mais permissões do que requests normais de usuários, pois trabalham com service_role ou conexão direta com o banco. Por isso, validação de input, delimitação de escopo e idempotência devem ser implementadas conscientemente em cada task.
A combinação da lógica de retry integrada do Trigger.dev, limites de concurrency conscientes e análise contextual do Claude Code resulta em workflows de background estáveis que funcionam corretamente mesmo em caso de falhas parciais.
Quem segue esses princípios junto com uma arquitetura Cert-Ready-by-Design constrói segurança verificável em vez de auditorias posteriores.
Download da checklist de auditoria
Prompt preparado para o Claude Code. Faça upload do arquivo no seu servidor e inicie o Claude Code no diretório do projeto da sua configuração Trigger.dev. O Claude Code verificará automaticamente todos os pontos de segurança deste runbook e reportará APROVADO, AVISO ou CRÍTICO.
claude -p "$(cat claude-check-artikel-4-trigger-dev-br.md)" --allowedTools Read,Grep,Glob,Bash
Baixar checklistSumário da série
- Supabase Self-Hosting Runbook
- Next.js sobre Supabase com segurança
- Supabase Edge Functions com segurança
- Trigger.dev Background Jobs com segurança - este artigo
- Claude Code como controle de segurança no workflow DevOps
- Security Baseline para todo o stack
O próximo artigo descreve como o Claude Code é utilizado como controle de segurança no workflow DevOps - como a camada de análise transversal sobre todos os artigos anteriores.

Bert Gogolin
Diretor Executivo, Gosign
AI Governance Briefing
IA empresarial, regulamentação e infraestrutura - uma vez por mês, diretamente de mim.