template-saas/docs/01-modulos/SAAS-010-webhooks.md
rckrdmrd 4dafffa386 feat: Add superadmin metrics, onboarding and module documentation
- 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>
2026-01-07 05:40:26 -06:00

245 lines
5.7 KiB
Markdown

# 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
1. Configuracion de webhooks por tenant
2. Eventos suscribibles
3. Firma HMAC de payloads
4. Reintentos con backoff
5. 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
```typescript
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)
```typescript
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
```typescript
@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
```typescript
@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