# Consent decisions recorded with timestamp, consent version, and categories accepted

- **Pattern:** `ab-001507` (`gdpr-readiness.consent-management.consent-records`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001507
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001507)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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.

```ts
// 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.

## Detection

- **ID:** `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:

  ```ts
  // 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.

---

## External references

- gdpr Art. 7(1)
- gdpr Art. 5(2)
- gdpr Art. 24

Taxons: regulatory-conformance, privacy-consent

HTML version: https://auditbuffet.com/patterns/ab-001507
