Data Lifecycle
Data Lifecycle Management
Version: 1.0 Date: 2026-02-21 Status: Approved Owner: Database Architect
Overview
Drop processes personal and financial data subject to multiple overlapping regulatory frameworks. This document defines retention periods, archival strategies, deletion cascades, and GDPR data subject request handling for all 19 tables.
Applicable regulations:
- GDPR (Personopplysningsloven, LOV-2018-06-15-38) -- data minimization, right to erasure, right to access
- AML/KYC (Hvitvaskingsloven, LOV-2018-06-01-23) -- 5-year retention post-relationship
- Norwegian Bookkeeping Act (Bokforingsloven) -- 5-year retention for financial records
- PSD2 (Betalingstjenesteloven) -- audit trail requirements
- Finansavtaleloven -- complaint handling records
Key tension: GDPR right to erasure (Art. 17) vs. AML legal retention obligations. AML wins -- data required for anti-money laundering must be retained for 5 years regardless of erasure requests.
Retention Periods
Per-Table Retention Schedule
| Table | Retention Period | Legal Basis | Archival After | Purge After |
|---|---|---|---|---|
users |
5 years post-relationship end | Hvitvaskingsloven section 30 | Account deletion + 1 year | 5 years post-deletion |
bank_accounts |
5 years post-relationship end | Hvitvaskingsloven section 30 | Account deletion | 5 years post-deletion |
transactions |
5 years from transaction date | Bokforingsloven section 13, Hvitvaskingsloven section 30 | 1 year after transaction | 5 years after transaction |
recipients |
5 years post-relationship end | Hvitvaskingsloven section 30 (counterparty records) | Account deletion | 5 years post-deletion |
merchants |
5 years post-relationship end | Bokforingsloven, Hvitvaskingsloven | Account deletion | 5 years post-deletion |
sessions |
90 days after expiry | Legitimate interest (security) | After expiry | 90 days after expiry |
notifications |
1 year from creation | Legitimate interest (UX) | 6 months | 1 year |
settings |
Duration of relationship | Contract performance | Account deletion | Immediate on deletion |
exchange_rates |
Indefinite (reference data) | Legitimate interest | Never | Never |
cards |
5 years post-cancellation | PCI-DSS, Bokforingsloven | Card cancellation | 5 years post-cancellation |
spending_limits |
Duration of card lifecycle | Contract performance | Card cancellation | With card record |
rate_limits |
Until window expires | Legitimate interest (security) | Auto-cleaned per request | Immediate on expiry |
audit_log |
5 years from event | PSD2 Art. 94, Hvitvaskingsloven | 1 year after event | 5 years after event |
aml_alerts |
5 years post-resolution | Hvitvaskingsloven section 30 | After resolution | 5 years post-resolution |
str_reports |
5 years after filing | Hvitvaskingsloven section 30 | Never (active reference) | 5 years after filing |
screening_results |
5 years post-relationship end | Hvitvaskingsloven section 30 | Account deletion | 5 years post-deletion |
consents |
Duration of consent + 5 years | GDPR Art. 7(1) (proof of consent) | After withdrawal + 1 year | 5 years after withdrawal |
data_access_requests |
5 years from completion | GDPR accountability (Art. 5(2)) | After completion | 5 years after completion |
complaints |
5 years from resolution | Finansavtaleloven, Bokforingsloven | After resolution | 5 years after resolution |
Per-Column Retention (Sensitive Fields)
| Table.Column | Contains | Retention | Anonymization Method |
|---|---|---|---|
users.email |
PII (email address) | Until erasure (then anonymized) | Replace with deleted_usr_{hash}@anonymized.local |
users.first_name |
PII | Until erasure | Replace with [REDACTED] |
users.last_name |
PII | Until erasure | Replace with [REDACTED] |
users.phone |
PII | Until erasure | Replace with NULL |
users.date_of_birth |
PII | Until erasure | Replace with NULL |
users.national_id_hash |
PII (hashed) | 5 years (AML) | Already hashed; set to NULL after retention |
users.password_hash |
Auth credential | Until erasure | Replace with DELETED |
bank_accounts.account_number |
Financial PII | 5 years (AML) | Replace with ****{last4} |
bank_accounts.iban |
Financial PII | 5 years (AML) | Replace with ****{last4} |
recipients.bank_account |
Financial PII | 5 years (AML) | Replace with ****{last4} |
recipients.name |
PII | 5 years (AML, counterparty) | Replace with [REDACTED] |
cards.last_four |
Financial (partial) | 5 years | Already truncated |
cards.pin_hash |
Auth credential | Until card cancellation | Set to NULL |
audit_log.ip_address |
PII (IP address) | 5 years (PSD2) | Replace with 0.0.0.0 after retention |
audit_log.user_agent |
Quasi-PII | 5 years | Replace with [REDACTED] after retention |
consents.ip_address |
PII | 5 years (proof of consent) | Replace with 0.0.0.0 after retention |
Archival Strategy
Active vs. Archived Data
flowchart LR
A[Active Data<br/>Primary Database] -->|After retention trigger| B[Cold Archive<br/>Read-only Storage]
B -->|After full retention period| C[Purge<br/>Permanent Deletion]
subgraph "Active (PostgreSQL)"
A1[Recent transactions]
A2[Active users]
A3[Current sessions]
end
subgraph "Cold Archive (S3/Glacier)"
B1[Old transactions > 1 year]
B2[Deleted user records]
B3[Resolved AML alerts]
B4[Filed STR reports]
end
subgraph "Purge"
C1[Records past 5-year retention]
C2[Anonymized analytics retained]
end
Archival Tiers
| Tier | Storage | Access Time | Data Types | Cost |
|---|---|---|---|---|
| Hot (Active DB) | PostgreSQL | Milliseconds | All current data, active users, recent transactions | Primary DB cost |
| Warm (Archive DB) | PostgreSQL read replica or separate schema | Seconds | Transactions > 1 year, deleted users pending retention | Reduced compute |
| Cold (Object storage) | AWS S3 / Glacier | Minutes to hours | Compliance exports, old audit logs, filed STR reports | Minimal |
Archival Process
- Daily job: Identify records eligible for archival (past active retention period)
- Export: Write eligible records to archive storage (S3 with server-side encryption)
- Verify: Confirm archive integrity (checksum comparison)
- Remove from active: Delete from primary database
- Log: Record archival action in
audit_log
Deletion Cascades: User Account Deletion
When a user requests account deletion (GDPR Art. 17 right to erasure), the following cascade executes:
flowchart TD
A[DELETE /api/user/account] --> B{Active transactions?}
B -->|Yes, processing| C[Reject: Wait for completion]
B -->|No| D[Begin deletion cascade]
D --> E[Revoke all sessions]
E --> F[Soft-delete user record]
F --> G[Anonymize PII fields]
G --> H[Create data_access_request<br/>type=erasure, status=completed]
subgraph "Immediate Actions"
E
F
G
end
subgraph "Retained for AML (5 years)"
I[transactions — amounts, dates, types]
J[audit_log — anonymized entries]
K[aml_alerts — if any]
L[str_reports — if any]
M[screening_results — if any]
end
subgraph "Deleted Immediately"
N[settings — preferences]
O[notifications — all]
P[rate_limits — if any for user IP]
end
subgraph "Anonymized + Retained"
Q[bank_accounts — account numbers masked]
R[recipients — names redacted]
S[consents — IP anonymized]
end
H --> I
H --> J
H --> K
H --> N
H --> Q
Deletion Cascade Detail
| Step | Table | Action | SQL |
|---|---|---|---|
| 1 | sessions |
Revoke all | UPDATE sessions SET revoked = 1 WHERE user_id = ? |
| 2 | users |
Soft delete + anonymize | UPDATE users SET deleted_at = CURRENT_TIMESTAMP, email = 'deleted_' || id || '@anonymized.local', first_name = '[REDACTED]', last_name = '[REDACTED]', phone = NULL, date_of_birth = NULL, password_hash = 'DELETED' WHERE id = ? |
| 3 | settings |
Delete | DELETE FROM settings WHERE user_id = ? |
| 4 | notifications |
Delete | DELETE FROM notifications WHERE user_id = ? |
| 5 | bank_accounts |
Anonymize | UPDATE bank_accounts SET account_number = '****' || RIGHT(account_number, 4), iban = CASE WHEN iban IS NOT NULL THEN '****' || RIGHT(iban, 4) END WHERE user_id = ? |
| 6 | recipients |
Anonymize | UPDATE recipients SET name = '[REDACTED]', bank_account = '****' || RIGHT(bank_account, 4) WHERE user_id = ? |
| 7 | consents |
Anonymize IP | UPDATE consents SET ip_address = '0.0.0.0' WHERE user_id = ? |
| 8 | cards |
Anonymize | UPDATE cards SET pin_hash = NULL WHERE user_id = ? |
| 9 | spending_limits |
Delete | DELETE FROM spending_limits WHERE user_id = ? |
| 10 | data_access_requests |
Create record | INSERT INTO data_access_requests (id, user_id, request_type, status, completed_at) VALUES (?, ?, 'erasure', 'completed', CURRENT_TIMESTAMP) |
| 11 | audit_log |
Log deletion | INSERT INTO audit_log (id, user_id, action, details) VALUES (?, ?, 'user.deleted', '{"reason":"gdpr_erasure"}') |
NOT deleted (AML retention): transactions, audit_log (existing entries), aml_alerts, str_reports, screening_results, merchants. These are retained for 5 years per hvitvaskingsloven section 30, with PII fields anonymized.
Data Subject Access Request (DSAR) Implementation
DSAR Types
| Request Type | GDPR Article | SLA | Implementation |
|---|---|---|---|
| Export (right to access) | Art. 15 | 30 days | GET /api/user/data-export -- returns JSON with all user data |
| Erasure (right to be forgotten) | Art. 17 | 30 days | DELETE /api/user/account -- soft delete + anonymization cascade |
| Rectification (right to correct) | Art. 16 | 30 days | POST /v1/user/rectification -- updates specified fields, creates data_access_request record |
| Restriction (right to restrict) | Art. 18 | 30 days | POST /v1/user/restriction -- flags account as restricted, creates data_access_request record |
Export Flow
sequenceDiagram
participant U as User
participant API as API
participant DB as Database
U->>API: GET /api/user/data-export
API->>DB: SELECT * FROM users WHERE id = ?
API->>DB: SELECT * FROM transactions WHERE user_id = ?
API->>DB: SELECT * FROM recipients WHERE user_id = ?
API->>DB: SELECT * FROM bank_accounts WHERE user_id = ?
API->>DB: SELECT * FROM settings WHERE user_id = ?
API->>DB: SELECT * FROM consents WHERE user_id = ?
API->>DB: INSERT INTO data_access_requests<br/>(type='export', status='completed')
API-->>U: 200 JSON { user, transactions, recipients, bankAccounts, settings, consents }
The current implementation (/api/user/data-export) returns data inline as JSON. For production, large exports should be written to a temporary signed S3 URL and the download_url field in data_access_requests populated.
DSAR Tracking
All DSARs are tracked in the data_access_requests table:
| Field | Purpose |
|---|---|
request_type |
export, erasure, rectification, restriction |
status |
pending -> processing -> completed/rejected |
requested_at |
When the user submitted the request |
completed_at |
When the request was fulfilled |
download_url |
Temporary URL for data export files |
notes |
Internal processing documentation |
Anonymization Techniques
For Analytics Retention
After the active retention period, data can be anonymized for analytics rather than deleted:
| Data Type | Anonymization Technique | Reversible? | Analytics Value |
|---|---|---|---|
| User identity | Replace name/email with opaque ID | No | User-level metrics without PII |
| Transaction amounts | Retain exact values (not PII) | N/A | Revenue and volume analytics |
| Geographic data | Retain country codes only | N/A | Corridor analysis |
| Timestamps | Retain date, remove time | Partially | Trend analysis |
| IP addresses | Replace with 0.0.0.0 |
No | None (removed for privacy) |
| Bank account numbers | Replace with ****{last4} |
No | None |
| Phone numbers | Remove entirely | No | None |
Anonymization SQL Pattern
-- Anonymize a deleted user's data for analytics retention
UPDATE users SET
email = 'anon_' || id || '@analytics.internal',
first_name = '[ANON]',
last_name = '[ANON]',
phone = NULL,
date_of_birth = NULL,
national_id_hash = NULL,
password_hash = 'ANONYMIZED'
WHERE id = ? AND deleted_at IS NOT NULL;
-- Transaction data is retained as-is (amounts are not PII)
-- Recipient names are redacted
UPDATE recipients SET
name = 'Recipient_' || id,
bank_account = '****' || SUBSTR(bank_account, -4)
WHERE user_id = ?;
Legal Basis Reference
| Retention Obligation | Law | Section | Requirement |
|---|---|---|---|
| KYC/AML records | Hvitvaskingsloven | Section 30 | Retain customer identity and transaction records for 5 years after relationship ends |
| Transaction records | Bokforingsloven | Section 13 | Retain accounting records for 5 years (3.5 years primary, 1.5 years secondary) |
| Audit trail | PSD2 / Betalingstjenesteloven | Art. 94 impl. | Maintain records of payment transactions for at least 5 years |
| Consent proof | GDPR | Art. 7(1) | Demonstrate that consent was given (retain proof) |
| Complaint records | Finansavtaleloven | Section 3-53 | Maintain complaint records (15 business day response SLA) |
| Right to erasure exceptions | GDPR | Art. 17(3)(b) | Erasure does not apply when processing is necessary for compliance with legal obligation |
| Data minimization | GDPR | Art. 5(1)(c) | Do not retain data longer than necessary for stated purpose |
| STR records | Hvitvaskingsloven | Section 30 | STR reports and supporting documentation retained 5 years after filing |
Conflict resolution: When GDPR right to erasure conflicts with AML retention requirements, AML wins per GDPR Art. 17(3)(b). The user is informed that "data [is] retained for 5 years per AML requirements" in the deletion response.
Automated Lifecycle Jobs
| Job | Frequency | Action |
|---|---|---|
| Session cleanup | Daily | Delete expired sessions older than 90 days |
| Rate limit cleanup | Every 100 rate limit checks | Delete expired rate limit entries (implemented in middleware/rate-limit.ts) |
| Notification cleanup | Weekly | Archive notifications older than 6 months, delete older than 1 year |
| Audit log archival | Monthly | Move audit entries older than 1 year to cold storage |
| AML alert archival | Monthly | Archive resolved alerts older than 1 year |
| User data purge | Monthly | Permanently delete anonymized user data past 5-year retention |
| Consent proof archival | Monthly | Archive withdrawn consents older than 1 year |
Retention Cron Endpoint
The retention enforcement is implemented as GET /v1/cron/retention (see cron.ts). When triggered, it:
- User anonymization (5+ years post-deletion): Anonymizes PII fields (
email,first_name,last_name,phone,date_of_birth,national_id_hash,password_hash) for users deleted more than 5 years ago - Session cleanup: Deletes expired sessions older than 90 days
- OTP cleanup: Removes expired OTP codes (legacy table, wrapped in try/catch)
This endpoint should be called periodically (e.g., daily via external scheduler or cron job). It is not automatically scheduled within the application.
Cross-References
- Database schema: DATABASE-SCHEMA.md
- Database design: database-design.md
- Audit architecture: audit-architecture.md
- Compliance status: COMPLIANCE.md
- Security architecture: SECURITY-ARCHITECTURE.md
- GDPR API endpoints: API-REFERENCE.md (GDPR & Compliance section)
- Account deletion:
DELETE /api/user/accountin API-REFERENCE.md - Data export:
GET /api/user/data-exportin API-REFERENCE.md