Los audit logs son el registro de quién hizo qué y cuándo en tu app. Suenan aburridos hasta el día
que un cliente B2B te pregunta "¿quién borró mi proyecto?" o un investigador de seguridad detecta un
acceso sospechoso. Sin audit log: no tienes respuesta. Con audit log: ya sabes.
Esta guía explica cómo implementar audit logs en una app Next.js con Prisma de forma que sea útil en
producción y no se convierta en una tabla que crece sin control.
## Qué registrar
No registres todo. Eso satura la DB y mata el rendimiento. Registra:
- Cambios de estado críticos: creación/borrado de cuentas, cambios de plan, transferencia de
ownership
- Acciones de admin: ban/unban, impersonate, cambios de rol
- Operaciones sobre datos sensibles: exportación, lectura de información personal, cambio de
password
- Login (con éxito y fallido) y logout
- Cambios en facturación
NO registres:
- Cada GET de listado
- Lecturas habituales que no afectan a nadie
- Cambios cosméticos sin impacto
## Paso 1: schema
```prisma
model AuditLog {
id String @id @default(cuid())
createdAt DateTime @default(now())
userId String?
organizationId String?
action String // p.ej. 'user.banned', 'project.deleted'
resource String? // p.ej. 'project:abc-123'
metadata Json? // datos adicionales
ip String?
userAgent String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
@@index([organizationId, createdAt])
@@index([userId, createdAt])
@@index([action, createdAt])
}
```
`onDelete: SetNull` es importante: si el usuario se borra, el log queda. El audit log debe
sobrevivir al borrado de los actores.
## Paso 2: helper centralizado
`src/lib/audit/log.ts`:
```ts
import { db } from '@/lib/db/client';
import { headers } from 'next/headers';
export async function logAudit(params: {
userId?: string;
organizationId?: string;
action: string;
resource?: string;
metadata?: Record
;
}) {
const h = await headers();
const ip = h.get('x-forwarded-for') ?? h.get('x-real-ip') ?? null;
const userAgent = h.get('user-agent') ?? null;
await db.auditLog.create({
data: {
...params,
metadata: params.metadata as never,
ip,
userAgent,
},
});
}
```
Y se usa así:
```ts
await logAudit({
userId: session.user.id,
organizationId: orgId,
action: 'project.deleted',
resource: `project:${projectId}`,
metadata: { projectName: project.name },
});
```
## Paso 3: convención de nombres de acción
Usa siempre `recurso.verbo`:
- `user.created`, `user.banned`, `user.deleted`
- `project.created`, `project.deleted`, `project.transferred`
- `subscription.created`, `subscription.canceled`
- `auth.login.success`, `auth.login.failed`
Coherencia desde el día 1. Si cada dev nombra a su gusto, en 6 meses no encuentras nada.
## Paso 4: no bloqueES la operación
El audit log NO debe bloquear la operación principal. Si el log falla, la operación sigue.
Mal:
```ts
await db.project.delete({ where: { id } });
await logAudit({ action: 'project.deleted', resource: `project:${id}` });
```
Si `logAudit` falla, lanza excepción y la respuesta al cliente es error → pero el proyecto YA se
borró. Cliente confundido.
Bien:
```ts
await db.project.delete({ where: { id } });
logAudit({ action: 'project.deleted', resource: `project:${id}` }).catch(console.error);
```
O mejor: cola asíncrona (Inngest, BullMQ) para audit logs.
## Paso 5: vista para admin
```tsx
'use client';
export function AuditLogTable({ logs }: { logs: AuditLog[] }) {
return (
| Fecha |
Usuario |
Acción |
Recurso |
IP |
{logs.map((log) => (
| {log.createdAt.toLocaleString()} |
{log.user?.email ?? 'system'} |
{log.action}
|
{log.resource} |
{log.ip} |
))}
);
}
```
Con filtros por usuario, fecha, acción y org.
## Paso 6: retención
Los logs crecen. Decide cuánto guardas:
- **30 días**: típico para apps con poco compliance
- **1 año**: B2B con clientes preguntando
- **7 años**: regulación financiera o sanitaria
Job programado que borra logs viejos:
```ts
await db.auditLog.deleteMany({
where: { createdAt: { lt: subDays(new Date(), 365) } },
});
```
Si necesitas guardar todo por compliance, exporta a S3 antes de borrar.
## Errores comunes
**1. Loggear contraseñas o tokens**: nunca metas en `metadata` campos sensibles. Filtra antes de
loggear.
**2. Tabla sin índices**: en producción con millones de filas, una query sin índice tarda minutos.
Indexa por `organizationId, createdAt`.
**3. Loggear desde middlewares ruidosos**: si loggeas cada request, en 1 mes tienes una tabla de
millones de filas inútiles. Selectivo.
## Conclusión
Audit logs son 1-2 horas de implementación si lo haces desde el principio, o un dolor enorme si lo
añades cuando ya hay datos en producción. Vale la pena hacerlo el día 1.
Y cuando un cliente pregunte "¿quién hizo X?", tendrás respuesta. Eso, en B2B, es oro.¿Te gustó este artículo?
Suscríbete para más tutoriales y tips sobre crear productos con IA