# Consent decisions recorded with timestamp, version, categories

- **Pattern:** `ab-000803` (`cookie-consent-compliance.consent-enforcement.consent-records-timestamped`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000803
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000803)

## Why it matters

GDPR Art. 7(1) places the burden of proof on the controller to demonstrate that consent was obtained. Art. 5(2) (accountability) reinforces this: the controller must be able to show, on request, that consent was lawful. A stored boolean `'true'` fails this standard — it records that something was accepted, not when, under what banner version, or for which specific purposes. Recital 42 notes that consent should include the time of consent and information about what was consented to. Without a timestamped, structured record, the controller cannot demonstrate compliance to a DPA or defend against a subject access request.

## Severity rationale

Medium because absent timestamp and version metadata makes it impossible to demonstrate lawful consent under GDPR Art. 7(1) and Art. 5(2), but the underlying processing may still be lawful if consent was genuinely given.

## Remediation

Store a structured consent record that includes per-category decisions, timestamp, version, and the method of consent. This record supports both re-consent logic and regulatory accountability.

```ts
// src/lib/consent.ts
export type ConsentRecord = {
  necessary: true
  analytics: boolean
  marketing: boolean
  timestamp: string  // ISO 8601
  version: string
  method: 'accept_all' | 'reject_all' | 'custom'
}

// On Accept All:
saveConsent({ analytics: true, marketing: true }, 'accept_all')
// On Reject All:
saveConsent({ analytics: false, marketing: false }, 'reject_all')
// On custom save:
saveConsent({ analytics, marketing }, 'custom')
```

For authenticated users, send this record to your server (a `consent_records` table) so you can produce an audit trail across devices and sessions if a regulator requests it.

## Detection

- **ID:** `consent-records-timestamped`
- **Severity:** `medium`
- **What to look for:** Inspect the stored consent object structure. Find where `localStorage.setItem` (or the equivalent cookie set) is called in the consent save handler. Check the shape of the stored value — does it include: (1) per-category booleans (analytics, marketing, etc.), (2) a timestamp (ISO 8601 string of when the user made their choice), (3) a consent version (to trigger re-consent when cookies change), (4) optionally the user's decision method ("accept_all", "reject_all", "custom"). Regulators (GDPR Article 7, Recital 42) require that the controller be able to demonstrate that the data subject consented — having a timestamped record is part of that demonstration. Check whether the stored consent can be retrieved and displayed to the user (e.g., in a cookie preferences page showing "You last consented on [date]").
- **Pass criteria:** Count all consent record fields stored. The stored consent object includes at minimum: per-category decisions (not just a single boolean), a timestamp, and a version. Ideally also the consent method (accept_all vs. custom). The format is parseable and the timestamp is a valid ISO 8601 string. At least 3 fields must be recorded: timestamp, consent version, and user choices.
- **Fail criteria:** Consent stored as a single boolean (`localStorage.setItem('consented', 'true')`). No timestamp in the stored object. No per-category breakdown in the stored record — just "accepted" or "declined" globally.
- **Skip (N/A) when:** No consent banner exists (already failing at `banner-first-visit`).
- **Detail on fail:** Example: `"Consent stored as a single string 'true' at key 'cookieConsent'. No per-category breakdown, no timestamp, no version."` or `"Consent object has {analytics: true} but no timestamp or version field."`.
- **Remediation:** Store a structured consent record. The `lib/consent.ts` implementation in the `consent-state-persisted` remediation already includes `timestamp` and `version`. Additionally capture the consent method:

  ```ts
  export type ConsentRecord = {
    necessary: true
    analytics: boolean
    marketing: boolean
    timestamp: string       // ISO 8601 — when user consented
    version: string         // banner version at time of consent
    method: 'accept_all' | 'reject_all' | 'custom'  // how user chose
  }

  // When user clicks "Accept All":
  saveConsent({ analytics: true, marketing: true }, 'accept_all')

  // When user clicks "Reject All":
  saveConsent({ analytics: false, marketing: false }, 'reject_all')

  // When user saves custom preferences:
  saveConsent({ analytics, marketing }, 'custom')
  ```

  This record can also be sent to your server for authenticated users, providing an audit trail demonstrating that consent was obtained.

---

## External references

- gdpr Art. 7(1)
- gdpr Recital 42
- gdpr Art. 5(2)

Taxons: privacy-consent, regulatory-conformance

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