gdpr
gdprdata-retentionpostgresqlautomation

GDPR Data Retention Policies in PostgreSQL: Automate the Cleanup

Automate GDPR Article 5(1)(e) data retention in PostgreSQL. Define per-table policies, schedule enforcement, and log every deletion.

RL
Robert Langner
Managing Director, NILS Software GmbH · · 4 min read

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

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.

Frequently Asked Questions

What retention periods does GDPR require?
GDPR does not specify exact retention periods — it requires data to be kept only as long as necessary for the stated purpose. You must define and document your own periods based on business need and legal requirements. Common examples: session data 30 days, support tickets 2 years, invoices 10 years (German HGB).
How do retention policies interact with the right to erasure?
Retention policies handle bulk cleanup of expired data. The right to erasure (Article 17) handles individual deletion requests. Both are needed: retention policies prevent data accumulation, while forget() handles on-demand DSAR requests. pgcomply provides both.
What happens if legal retention requirements conflict with GDPR?
Legal obligations override GDPR deletion requirements. German tax law (HGB §257, AO §147) requires invoices and financial records to be kept for 10 years. In these cases, use the null forget strategy to remove PII while keeping the record for the legally mandated period.

Related Articles