PCI-DSS 4.0 Req 3.2 limits cardholder data storage to the minimum required for business or legal needs; Req 3.2.1 requires that data retention and disposal policies are defined and implemented. Data that is never deleted accumulates indefinitely — old failed transaction records, expired tokens, and support tickets create a growing pool of sensitive data that broadens breach impact year over year. GDPR Art. 5(1)(e) (storage limitation) and NIST SI-12 both require data to be disposed of when no longer needed. Without automated purging, retention policies are aspirational rather than enforced.
Low because data accumulation expands breach impact over time but does not itself enable unauthorized access — the risk compounds gradually rather than presenting an immediate exploitable condition.
Create docs/data-retention-policy.md with specific retention periods for at least three data types, then implement automated purging via a Vercel cron route or database scheduled job. Without automation, the policy is not enforced.
// src/app/api/cron/purge-data/route.ts
export async function POST(req: Request) {
if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`)
return new Response('Unauthorized', { status: 401 });
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!);
// Purge failed transactions older than 90 days (PCI Req 3.2)
await supabase.from('failed_transactions').delete()
.lt('created_at', new Date(Date.now() - 90 * 864e5).toISOString());
return new Response('ok');
}
In vercel.json, schedule the route at "cron": "0 3 * * *". Define transaction logs (7 years), audit logs (3 years), and failed attempts (90 days) as the minimum three retention categories.
ID: ecommerce-pci.monitoring-compliance.data-retention-policy
Severity: low
What to look for: Search for data retention documentation (look for "retention", "purge", "data-lifecycle" in docs/ and project root). Count the number of data types with defined retention periods. For each data type, check whether a specific retention duration is stated (not "indefinite" or "as needed"). Look for automated purging code: cron jobs, scheduled functions, or database TTL configurations that delete data beyond retention periods.
Pass criteria: At least 1 data retention policy document exists that defines retention periods for at least 3 data types with specific durations (e.g., "transaction logs: 7 years", "failed attempts: 90 days"). At least 1 automated purging mechanism is implemented (cron job, scheduled function, or database TTL that deletes expired data). Report: "X data types with defined retention, Y automated purging mechanisms."
Fail criteria: No retention policy documented (0 files), or policy exists but covers fewer than 3 data types, or data types lack specific durations, or 0 automated purging mechanisms implemented.
Skip (N/A) when: No cardholder data or payment-related data stored locally (all data handled by third-party processor, no local database storing transaction or card data).
Detail on fail: Specify the gap. Example: "No data retention policy found. 0 documentation files reference retention periods." or "docs/data-retention.md defines 2 data types (below 3 minimum). No automated purging code found (0 cron jobs, 0 TTL configs)."
Remediation: Create data retention policy and implement automated purging. Create docs/data-retention-policy.md:
# Data Retention Policy
## Retention Periods by Data Type
| Data Type | Retention Period | Justification | Notes |
|-----------|------------------|---------------|-------|
| Transaction logs | 7 years (2555 days) | Tax/audit requirements | Immutable archive |
| Audit logs | 3 years (1095 days) | PCI DSS requirement | Searchable database |
| Customer communication logs | 1 year (365 days) | Customer service | Archived after 1 year |
| Payment method tokens | Duration of relationship | Active customer card | Can be deleted on request |
| Billing/invoice data | 7 years | Tax records | Archival storage |
| Failed transaction attempts | 90 days | Fraud investigation | Then deleted |
| Support tickets | 1 year | Customer support reference | Then archived |
Implement automated purging:
// src/app/api/cron/purge-data/route.ts
import { createClient } from '@/utils/supabase/server';
export async function POST(req) {
// Verify cron secret
if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
const supabase = await createClient();
// Purge failed transactions older than 90 days
await supabase
.from('failed_transactions')
.delete()
.lt('created_at', new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString());
// Purge support tickets older than 1 year
await supabase
.from('support_tickets')
.update({ archived: true })
.lt('created_at', new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString());
// Archive audit logs older than 3 years to cold storage
const archivedLogs = await supabase
.from('audit_logs')
.select('*')
.lt('created_at', new Date(Date.now() - 1095 * 24 * 60 * 60 * 1000).toISOString())
.limit(10000);
if (archivedLogs.data?.length > 0) {
// Export to S3 cold storage
await s3.putObject({
Bucket: 'audit-log-archive',
Key: `archive-${new Date().toISOString().split('T')[0]}.json.gz`,
Body: gzip(JSON.stringify(archivedLogs.data)),
});
}
return new Response(JSON.stringify({ success: true, purged: true }));
}