Owner: Security. Review cadence: annual or on material change. Approver: CEO.
This policy formalizes how long each class of data MeterBox holds is kept,
where the TTL is enforced, and why. The machine-readable form is returned by
GET /cp/retention and is the same table the SECURITY.md page renders for
tenants — those, this doc, and
src/control-plane/services/retention-policy.js
must stay in sync. The retention-policy module IS the source of truth: change
a TTL there and every surface follows.
Customers, regulators, and tenants ask: "how long do you keep my data, and who removes it?" This policy answers, by category. It applies to every data class MeterBox persists in the control plane, every tenant's data plane, and the off-cluster backup mirror.
The authoritative table lives in
retention-policy.js
and is surfaced via GET /cp/retention. Reproduced here with rationale:
| Class | TTL | Where enforced | Rationale |
|---|---|---|---|
Audit log — tenant lifecycle (cp_audit_events) |
365 days (default; configurable via AUDIT_RETENTION_DAYS) |
audit-retention-sweeper.js calls audit.pruneOlderThan(cutoff) |
NIST AU-11 requires ≥ 90 days online; SOC 2 + customer forensic needs make a year a sensible default. Append-only + hash-chained; pruning is the only deletion path. |
Audit log — billing-ops (/v1/audit, per env) |
365 days (same AUDIT_RETENTION_DAYS window) |
billing-audit-retention-sweeper.js calls billing-audit.pruneOlderThan(cutoff) per env |
Same AU-11 / SOC 2 basis as the control-plane log. Append-only + hash-chained; age-only pruning is the sole deletion path, and the chain tolerates the resulting post-prune genesis. |
| Outbound webhook delivery log | 30 days (terminal rows) | Webhook dispatcher prune | Long enough to debug a failed delivery, short enough to bound storage cost. |
| Session — idle | 1800s sliding (configurable via SESSION_IDLE_TTL_SECONDS) |
Redis TTL on session key | Auto-logout for inactive operators. |
| Session — absolute | 43200s from login (configurable via SESSION_ABSOLUTE_TTL_SECONDS) |
Redis TTL on session key | Forces a fresh authentication even on continuously active sessions. |
| Usage ledger | Lifetime of the account | Data-plane PVC, no auto-prune | Tenants build invoices + statements from full history; truncating it breaks billing. |
| Credit grants | Lifetime; expired grants flipped to expired, never deleted |
Data-plane records | Customer-facing balance history requires the full series. |
| Reclaim backups (tarballs) | Operator-managed; no auto-prune | Control-plane PV at BACKUP_PATH |
Operator decides the off-host snapshot policy. Default behaviour is "keep until the operator removes." |
| Backup mirror snapshots | Per tenant's cloud bucket lifecycle policy | Tenant's bucket | MeterBox writes; the tenant's bucket lifecycle policy is the system of record. |
| Email verification tokens | 24 hours (one-shot) | Verification service | Short-lived per OWASP token guidance. |
| Password-reset tokens | 1 hour (one-shot) | Password-reset service | Short-lived; one-shot. |
| MFA challenge tokens | 5 minutes (one-shot) | MFA service | Bound to the second-factor exchange window. |
| Invites | 14 days (configurable) | Invites service | Long enough for new-hire onboarding, short enough that a leaked link is bounded. |
| CSRF tokens | Session lifetime | Double-submit cookie | Bound to the authenticated session. |
Each TTL has exactly one enforcement point:
audit-retention-sweeper.js runs
daily; calls audit.pruneOlderThan(cutoff) using AUDIT_RETENTION_DAYS.billing-audit-retention-sweeper.js runs in
the data-plane process on the same interval; prunes /v1/audit per env
using the same AUDIT_RETENTION_DAYS window.null.If a sweeper is paused (operator action), the data is kept; once resumed, the next sweep brings retention back to the policy.
The DSAR-erasure flow at
src/customer-dsar.js
anonymizes a customer record on request (Art. 17). Financial records
(ledger_entries, credit_grants, invoices) are retained under the
legal-obligation exception in Art. 17(3)(b) — they reference the
non-PII external customer id, so keeping them does not re-identify the
subject. This is consistent with the privacy page
processor framing.
Tax + accounting law in most jurisdictions requires invoice + ledger retention for 5–10 years; MeterBox does not auto-delete these.
GET /v1/customers/:id/dsar/export) returns everything
we hold on the subject at the moment of the call.POST /v1/customers/:id/dsar/erase) anonymizes PII and
stamps erased_at; subsequent metering is blocked. Financial records
remain (see § 4).GET /cp/access-review) returns the org's team
posture derived from the audit log; values older than
AUDIT_RETENTION_DAYS surface as null.A tenant or regulator may require a different TTL on a category (e.g. a financial regulator may require audit retention > 1 year). Process:
configurable_via (or, for category overrides at the tenant level, by
per-tenant configuration).This policy is reviewed annually or on material change (new data class,
new regulator requirement, sweeper rewrite). The retention table here is
re-derived from retention-policy.js at review time so the doc cannot
drift from the running code.
Last reviewed: 2026-06-04
retention-policy.js — code source of truth.