The Accumulation Problem
Without retention policies, databases grow indefinitely. Session logs from three years ago, expired password reset tokens, abandoned cart data — all still sitting in your tables. Each row is a compliance liability.
Defining Retention Policies
-- Sessions: 30 days
SELECT pgcomply.retain('sessions', 'created_at', '30 days');
-- Password reset tokens: 24 hours
SELECT pgcomply.retain('password_resets', 'created_at', '24 hours');
-- Support tickets: 2 years
SELECT pgcomply.retain('support_tickets', 'closed_at', '2 years');
-- Audit logs: keep longer for compliance evidence
SELECT pgcomply.retain('audit_log', 'created_at', '7 years', p_schema_name := 'pgcomply');
Enforcing Retention
Manual Enforcement
SELECT pgcomply.enforce_retention();
Output:
NOTICE: pgcomply.retain: sessions — 1,247 rows deleted (older than 30 days)
NOTICE: pgcomply.retain: password_resets — 89 rows deleted (older than 24 hours)
NOTICE: pgcomply.retain: support_tickets — 0 rows (none past retention)
Automated Daily Enforcement
SELECT pgcomply.schedule_jobs(install := true);
-- Creates: daily at 03:00 — enforce_retention()
Or manually with pg_cron:
SELECT cron.schedule('retention-daily', '0 3 * * *',
'SELECT pgcomply.enforce_retention()');
Viewing Active Policies
SELECT * FROM pgcomply.retention_policies();
table_name | column_name | retention_period | last_enforced | rows_deleted_total
------------------+-------------+------------------+---------------------+-------------------
sessions | created_at | 30 days | 2026-02-22 03:00:01 | 45,892
password_resets | created_at | 24 hours | 2026-02-22 03:00:01 | 12,340
support_tickets | closed_at | 2 years | 2026-02-22 03:00:01 | 234
German Legal Requirements
In Germany, several laws mandate minimum retention:
| Data Type | Law | Period | |-----------|-----|--------| | Invoices, financial records | HGB §257, AO §147 | 10 years | | Business correspondence | HGB §257 | 6 years | | Employment records | various | 3-10 years | | Tax-relevant documents | AO §147 | 10 years |
For these tables, use null as the forget strategy to remove PII while keeping the record:
SELECT pgcomply.register_pii('invoices', 'customer_name', 'person_name', 'customer_id', 'null');
-- forget() will NULL the name but keep the invoice row
Häufige Aufbewahrungsfristen (EU / Deutschland)
| Datenkategorie | Frist | Rechtsgrundlage | |---|---|---| | Sitzungsdaten / Cookies | 30 Tage | Keine Pflicht | | Bewerberdaten | 6 Monate | AGG §15 | | Allgemeine Geschäftsdaten | 3 Jahre | §195 BGB (Verjährung) | | Geschäftsbriefe | 6 Jahre | HGB §257 | | Buchungsbelege | 10 Jahre | HGB §257, AO §147 | | Rechnungen | 10 Jahre | AO §147 | | Medizinische Unterlagen | 10-30 Jahre | §630f BGB |
These periods override GDPR erasure obligations — you may retain data that has a legal retention requirement, but must delete it once the requirement expires.
Monitoring Retention Health
Track whether retention is actually working:
-- Check enforcement history
SELECT event_type, created_at, details
FROM pgcomply.audit_log
WHERE event_type = 'retention_enforced'
ORDER BY created_at DESC LIMIT 10;
If no enforcement events appear in the last 7 days, your pg_cron job may have failed:
-- Check job status
SELECT jobid, schedule, command,
(SELECT max(start_time) FROM cron.job_run_details d WHERE d.jobid = j.jobid) AS last_run,
(SELECT status FROM cron.job_run_details d WHERE d.jobid = j.jobid ORDER BY start_time DESC LIMIT 1) AS last_status
FROM cron.job j
WHERE command LIKE '%enforce_retention%';
Edge Case: Retention vs Active Users
A common mistake: setting retention on a users table without considering active users. If you set retain('users', 'created_at', '365 days'), users who registered more than a year ago but are still active will be deleted.
The solution: use the last_active or last_login column instead:
-- Delete inactive users (no login in 2 years)
SELECT pgcomply.retain('users', 'last_login', '730 days');
-- Keep active users regardless of registration date
Summary
Retention policies are the automated complement to on-demand erasure. pgcomply.retain() defines per-table policies, enforce_retention() executes them, and the audit trail proves it happened. Schedule daily enforcement and your database stays clean without manual intervention.