Data Retention Policy

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.

1 — Purpose + scope

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.

2 — Retention table

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.

3 — Enforcement mechanism

Each TTL has exactly one enforcement point:

If a sweeper is paused (operator action), the data is kept; once resumed, the next sweep brings retention back to the policy.

4 — Legal-obligation exceptions

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.

5 — Interaction with DSAR + access reviews

6 — Operator overrides + exceptions

A tenant or regulator may require a different TTL on a category (e.g. a financial regulator may require audit retention > 1 year). Process:

  1. Tenant or Security lead raises the request.
  2. Security lead reviews against the rationale column above + applicable law.
  3. If approved, the override is applied via the env var named in configurable_via (or, for category overrides at the tenant level, by per-tenant configuration).
  4. The override is logged as a policy exception under the InfoSec policy § 12 exceptions process.

7 — Review + version control

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

Related docs