GDPR Art. 7(1) and Art. 5(2) (accountability) require that controllers be able to demonstrate that consent was validly obtained. A bare boolean in localStorage — accepted: true — is not evidence of anything: it records no timestamp, no consent version, no which categories were accepted, and is trivially forgeable or deleted. When a regulator or data subject challenges whether consent was given for a specific purpose at a specific time, you need a server-side audit trail tied to a versioned consent notice. Without it, you cannot meet the Art. 5(2) accountability obligation even if you collected consent in good faith.
High because unstructured or versionless consent records fail the Art. 7(1) burden-of-proof requirement — you cannot demonstrate lawful consent was given for each processing purpose, which undermines every consent-based processing activity.
Record structured consent decisions server-side for authenticated users, with timestamp, consent notice version, and per-category breakdown. Never store consent as a bare boolean.
// app/api/consent/route.ts
export async function POST(req: Request) {
const { analyticsAccepted, marketingAccepted, consentVersion, anonymousId } = await req.json()
const session = await getServerSession()
await db.consentRecord.create({
data: {
userId: session?.user?.id ?? null,
anonymousId: session ? null : (anonymousId ?? null),
consentVersion, // e.g. '2026-02-01' — tie to a versioned notice
analyticsAccepted,
marketingAccepted,
userAgent: req.headers.get('user-agent'),
}
})
return Response.json({ ok: true })
}
Append records — never overwrite. Include consentRecord in your DSAR export so users can see what they consented to and when. The consentVersion field is your proof-of-notice: it maps to the privacy policy version in effect at the moment of consent.
ID: gdpr-readiness.consent-management.consent-records
Severity: high
What to look for: Check how consent decisions are stored. localStorage alone is insufficient for GDPR audit purposes — consent records should ideally be persisted server-side for authenticated users so they survive device changes and can be produced in a DSAR. Look for: a consent_records or consent_log database table; server-side API calls that record consent when the banner is interacted with; storage of the timestamp of the decision, the version of the consent notice at the time of the decision, and which categories were accepted or rejected. For unauthenticated users, client-side storage (localStorage) may be acceptable as a pragmatic approach, but the consent version must still be recorded. Check whether old consent records are preserved (audit trail) or overwritten. Count all instances found and enumerate each.
Pass criteria: Consent decisions are recorded with at minimum: the timestamp of the decision, the version of the consent notice, and which categories were accepted. For authenticated users, records are persisted server-side. Consent records are preserved as an audit trail (not overwritten on update). Records can be retrieved as part of a DSAR response. At least 1 implementation must be confirmed.
Fail criteria: Consent is stored only as a boolean in localStorage with no timestamp, version, or category breakdown. No server-side consent records for authenticated users. Consent records are overwritten rather than appended. Do NOT pass if consent is collected but no timestamp, version, or scope is stored — bare boolean flags are insufficient evidence.
Skip (N/A) when: Application has no cookies or consent mechanism because it has no analytics, tracking, or non-essential processing.
Cross-reference: The consent-before-processing check in Lawful Basis verifies the consent UX that generates the records this check validates.
Detail on fail: Example: "Consent stored as localStorage.setItem('accepted', 'true') — no timestamp, no version, no category breakdown." or "No server-side consent records for authenticated users. Consent data lost on localStorage clear.".
Remediation: Record structured consent decisions server-side for authenticated users:
// Database schema (Prisma)
// model ConsentRecord {
// id String @id @default(uuid())
// userId String? // null for anonymous
// anonymousId String? // client-generated ID for anonymous users
// consentVersion String // e.g., "2026-02-01"
// analyticsAccepted Boolean
// marketingAccepted Boolean
// recordedAt DateTime @default(now())
// userAgent String?
// @@index([userId])
// }
// app/api/consent/route.ts — called when user saves consent preferences
export async function POST(req: Request) {
const { analyticsAccepted, marketingAccepted, consentVersion, anonymousId } =
await req.json()
const session = await getServerSession()
await db.consentRecord.create({
data: {
userId: session?.user?.id ?? null,
anonymousId: session ? null : (anonymousId ?? null),
consentVersion,
analyticsAccepted,
marketingAccepted,
userAgent: req.headers.get('user-agent'),
}
})
return Response.json({ ok: true })
}
Include consent records in DSAR exports. Never delete them — they are your proof of compliance.