Audit Janitor
Automatic log retention and purge. Keep storage costs controlled, satisfy LGPD/GDPR retention requirements, and maintain query performance at scale.
Audit Janitor
Why this exists
Audit logs that accumulate indefinitely create three problems:
- Cost: Cloudflare D1 charges per GB stored. A busy agent generating 1,000 decisions/day produces ~365K records/year — each with a full JSON payload.
- Performance: SQLite query time degrades as table row count grows without periodic maintenance.
- Compliance risk: LGPD and GDPR require data to be deleted once its retention purpose expires. Logs kept beyond the legal period are a liability, not an asset.
The Audit Janitor runs automatically on a daily schedule and purges records older than your configured retention period, with safeguards to never delete records that are still needed.
Default behavior
The Janitor starts automatically when the ABS server starts, with a 5-minute warmup delay. It runs every 24 hours.
Default retention: 90 days. Configure with the ABS_AUDIT_RETENTION_DAYS environment variable.
Tables managed automatically:
| Table | Default retention | Safeguard |
|---|---|---|
audit_logs | 90 days | Never deletes records referenced by pending human reviews |
token_usage | 90 days | — |
budget_violations | 90 days | — |
cortex_sequences | 90 days | — |
threat_intel_received | 7 days (TTL) | Enforced by expires_at column |
Configuration
Set retention via environment variable:
ABS_AUDIT_RETENTION_DAYS=365 # Keep 1 year (e.g. for SOC2 audits)
ABS_ALERT_WEBHOOK_URL=https://hooks.slack.com/... # Alert if purge failsOr configure per-tenant via the API:
PUT /v1/maintenance/retention
Authorization: Bearer {your-api-key}
{
"tenant_id": "acme-corp",
"retention_days": 365,
"alert_webhook_url": "https://hooks.slack.com/services/..."
}Valid range: 7 – 3650 days.
Check the last run
GET /v1/maintenance/janitor
Authorization: Bearer {your-api-key}{
"last_run": {
"run_at": "2026-02-21T03:00:00Z",
"retention_days": 90,
"total_deleted": 12847,
"duration_ms": 340,
"success": true,
"details": [
{ "table": "audit_logs", "deleted": 11200 },
{ "table": "token_usage", "deleted": 1580 },
{ "table": "budget_violations", "deleted": 67 },
{ "table": "cortex_sequences", "deleted": 0 }
]
},
"retention_config": {
"default_days": 90,
"per_tenant": []
}
}Trigger a manual purge
Useful before a scheduled compliance audit or after a large bulk import:
POST /v1/maintenance/janitor/run
Authorization: Bearer {your-api-key}
{
"retention_days": 90
}Dry run (preview before deleting)
Check how many records would be deleted without actually deleting anything:
POST /v1/maintenance/janitor/run
Authorization: Bearer {your-api-key}
{
"dry_run": true,
"retention_days": 90
}{
"dry_run": true,
"retention_days": 90,
"cutoff": "2025-11-23T00:00:00Z",
"would_delete": {
"audit_logs": 8500,
"token_usage": 920,
"budget_violations": 12,
"cortex_sequences": 0
}
}View run history
GET /v1/maintenance/janitor/historyReturns the last 30 Janitor runs — useful for confirming the schedule is running and diagnosing failures.
Invariants
- AJ-I1: Retention period is configurable per tenant. Global default: 90 days.
- AJ-I2: Audit logs referenced by pending human reviews are never deleted, regardless of age.
- AJ-I3: Every purge run is logged to
audit_janitor_runsfor accountability. - AJ-I4: If a purge fails, an alert is sent to the configured webhook URL.
- AJ-I5: The Janitor runs with a 5-minute startup delay to avoid interfering with server initialization.