- Add MetricsPage and useOnboarding hook - Update superadmin controller and service - Add module documentation (docs/01-modulos/) - Add CONTEXT-MAP.yml and Sprint 5 execution report - Update project status and task traces 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
5.7 KiB
5.7 KiB
SAAS-010: Webhooks
Metadata
- Codigo: SAAS-010
- Modulo: Webhooks
- Prioridad: P2
- Estado: Pendiente
- Fase: 5 - Integraciones
Descripcion
Sistema de webhooks outbound: configuracion de endpoints por tenant, eventos suscribibles, firma de payloads, reintentos automaticos, y logs de entregas.
Objetivos
- Configuracion de webhooks por tenant
- Eventos suscribibles
- Firma HMAC de payloads
- Reintentos con backoff
- Dashboard de entregas
Alcance
Incluido
- CRUD de webhooks por tenant
- Eventos de sistema suscribibles
- Firma HMAC-SHA256
- Reintentos exponenciales (max 5)
- Logs de entregas
- Test endpoint
Excluido
- Transformacion de payloads
- Webhooks inbound (recibir)
- Fanout a multiples endpoints por evento
Modelo de Datos
Tablas (schema: webhooks)
webhooks
- id, tenant_id, name
- url, secret (encrypted)
- events (JSONB array)
- headers (JSONB)
- is_active, created_at
webhook_deliveries
- id, webhook_id, event_type
- payload (JSONB)
- response_status, response_body
- attempt, next_retry_at
- delivered_at, created_at
Eventos Disponibles
| Evento | Descripcion | Payload |
|---|---|---|
| user.created | Usuario creado | User object |
| user.updated | Usuario actualizado | User + changes |
| user.deleted | Usuario eliminado | { userId } |
| subscription.created | Nueva suscripcion | Subscription |
| subscription.updated | Suscripcion cambiada | Subscription |
| subscription.cancelled | Suscripcion cancelada | Subscription |
| invoice.paid | Factura pagada | Invoice |
| invoice.failed | Pago fallido | Invoice |
Endpoints API
| Metodo | Endpoint | Descripcion |
|---|---|---|
| GET | /webhooks | Listar webhooks |
| GET | /webhooks/:id | Obtener webhook |
| POST | /webhooks | Crear webhook |
| PUT | /webhooks/:id | Actualizar webhook |
| DELETE | /webhooks/:id | Eliminar webhook |
| POST | /webhooks/:id/test | Enviar test |
| GET | /webhooks/:id/deliveries | Historial entregas |
| POST | /webhooks/:id/deliveries/:did/retry | Reintentar |
| GET | /webhooks/events | Eventos disponibles |
Firma de Payloads
Generacion
function signPayload(payload: object, secret: string): string {
const timestamp = Date.now();
const body = JSON.stringify(payload);
const signature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');
return `t=${timestamp},v1=${signature}`;
}
Headers Enviados
X-Webhook-Signature: t=1704067200000,v1=abc123...
X-Webhook-Id: wh_123456
X-Webhook-Event: user.created
X-Webhook-Timestamp: 1704067200000
Verificacion (lado receptor)
function verifySignature(payload: string, signature: string, secret: string): boolean {
const [timestamp, hash] = parseSignature(signature);
// Verificar timestamp (5 min tolerance)
if (Date.now() - timestamp > 300000) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(expected)
);
}
Logica de Reintentos
Intento 1: Inmediato
Intento 2: +1 minuto
Intento 3: +5 minutos
Intento 4: +30 minutos
Intento 5: +2 horas
Despues: Marcar como fallido
Status de Entrega
| Status | Descripcion |
|---|---|
| pending | En cola |
| delivered | Entregado (2xx) |
| failed | Fallo permanente |
| retrying | En reintento |
Implementacion
Dispatcher
@Injectable()
export class WebhookDispatcher {
async dispatch(tenantId: string, event: string, data: object): Promise<void> {
const webhooks = await this.getActiveWebhooks(tenantId, event);
for (const webhook of webhooks) {
await this.webhookQueue.add('deliver', {
webhookId: webhook.id,
event,
payload: data,
attempt: 1
});
}
}
}
Worker
@Processor('webhooks')
export class WebhookWorker {
@Process('deliver')
async deliver(job: Job<WebhookJob>): Promise<void> {
const { webhookId, event, payload, attempt } = job.data;
const webhook = await this.getWebhook(webhookId);
const signature = this.signPayload(payload, webhook.secret);
try {
const response = await axios.post(webhook.url, payload, {
headers: {
'X-Webhook-Signature': signature,
'X-Webhook-Event': event,
...webhook.headers
},
timeout: 30000
});
await this.logDelivery(webhookId, event, payload, response, 'delivered');
} catch (error) {
await this.handleFailure(job, error);
}
}
}
Limites por Plan
| Plan | Webhooks | Eventos/mes |
|---|---|---|
| Free | 0 | 0 |
| Starter | 0 | 0 |
| Pro | 5 | 10,000 |
| Enterprise | 20 | 100,000 |
Entregables
| Entregable | Estado | Archivo |
|---|---|---|
| webhooks.module.ts | Pendiente | modules/webhooks/ |
| webhook.service.ts | Pendiente | services/ |
| webhook.dispatcher.ts | Pendiente | services/ |
| webhook.worker.ts | Pendiente | workers/ |
| DDL webhooks schema | Pendiente | ddl/schemas/webhooks/ |
Dependencias
Depende de
- SAAS-002 (Tenants)
- SAAS-005 (Plans - feature flag)
- BullMQ para queue
Bloquea a
- Integraciones de terceros
- Automatizaciones externas
Criterios de Aceptacion
- CRUD webhooks funciona
- Eventos se disparan
- Firma es correcta
- Reintentos funcionan
- Test endpoint funciona
- Logs se registran
Seguridad
- Secrets encriptados en BD
- HTTPS requerido para URLs
- Timeout de 30 segundos
- Rate limit por tenant
- No seguir redirects
Ultima actualizacion: 2026-01-07