Blog
Written by the Erasure product and engineering team
Part of Consent management
Consent Database Schema: How to Store Consent So You Can Prove It Later
A consent record that survives scrutiny needs three things: what the user saw, what they chose, and when. Here is a schema design that keeps all three, append-only.
The hardest part of consent is not collecting it. It is proving, six months later, what a specific person saw and chose. Most consent tables only store "accepted" and "at," which proves nothing. Here is a schema design that actually holds up.
The three things a consent record must capture
A defensible consent record answers three questions:
- What did they see? Not "the cookie banner" in general—the specific version of the notice, the list of purposes, the wording, as it existed at that moment.
- What did they choose? Per-purpose yes or no, not one lumpen "accepted."
- When, and by whom? A timestamp, and some way to correlate the decision to a device or user.
If your schema cannot answer all three, it stores the outcome but not the consent.
The two-table design
Keep the version (what they saw) separate from the decision (what they chose). Mixing them is how notices silently change and the old wording disappears.
-- One row per published consent configuration.
CREATE TABLE consent_versions (
id BIGSERIAL PRIMARY KEY,
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
snapshot JSONB NOT NULL, -- full notice + purpose definitions
content_hash TEXT NOT NULL -- hash of the snapshot, for integrity
);
-- One row per decision. Append-only: never UPDATE, never DELETE.
CREATE TABLE consent_records (
id BIGSERIAL PRIMARY KEY,
version_id BIGINT NOT NULL REFERENCES consent_versions(id),
device_id TEXT, -- installation / device correlation, not PII
purpose_key TEXT NOT NULL, -- e.g. 'marketing', 'analytics'
granted BOOLEAN NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_consent_records_device
ON consent_records (device_id, purpose_key);
The version_id foreign key is the whole point. A change to the notice creates a new version row; the old row never changes. Old decisions keep pointing at the exact version the person saw.
The rules that make it hold up
- Append-only. Never
UPDATEa consent record. A change of mind is a new row with the new choice. Rewriting history is the fastest way to invalidate your evidence. - Snapshot the notice. Store the full notice content in the version row. A reference to "the notice" is worthless if the notice is an editable document somewhere else.
- Hash the content. Store a hash of the snapshot so a tampered-with record is detectable. The receipt should also carry the hash of what was shown, so the two can be cross-checked.
- Per-purpose choice. Store
purpose_keyandgrantedper row, not one boolean per user. "They clicked accept" means nothing unless you know what the accept covered, and required purposes should never be toggleable off. - Don't store what you don't need. For the core path you do not need an email or IP address on the receipt—you need a device identifier to correlate choices and a timestamp. Every unnecessary field is data you now have to protect and delete on request.
Withdrawal is a new record, not an edit
When a user withdraws or changes optional purposes, insert new rows:
INSERT INTO consent_records (version_id, device_id, purpose_key, granted, recorded_at)
VALUES
(7, 'device-a1b2', 'marketing', FALSE, NOW()),
(7, 'device-a1b2', 'analytics', FALSE, NOW());
The history stays intact: earlier rows show consent was given, later rows show it was withdrawn. A reviewer can reconstruct the full timeline instead of trusting the latest state.
Idempotency and duplicates
The SDK and your retry logic may deliver the same decision twice. Add a natural dedup key—for example a client-generated decision ID—and make the insert ignore duplicates:
CREATE UNIQUE INDEX idx_consent_unique_decision
ON consent_records (device_id, purpose_key, decision_uid);
Without this, a retried POST creates two rows for one decision and your counts drift.
The pattern in Erasure
This is the same shape as Erasure's Accord: an immutable published consent version with a content hash, append-only consent receipts that lock to the version the person saw, per-purpose choice, withdrawal that creates a new receipt, and a device-scoped identifier rather than full subject accounts. A consent.updated webhook tells your backend when choices change so downstream systems can react.
For more on the product side, the consent management hub covers the workflow, and the receipts documentation explains the exact model.
About this post
Written by the Erasure product and engineering team
Published 5 August 2026
Part of Consent management
This article is grounded in Erasure's product documentation and explains engineering and operational implications. Where it discusses regulation, it is not legal advice. See our editorial policy.
More on consent management
Verifiable Parental Consent Under DPDP: What It Means for Your Product
DPDP defines a child as under 18 and requires verifiable parental consent for processing a child's data. Behavioural monitoring and targeted ads to children are prohibited outright.
Cookie Banners in India: DPDP Notice vs GDPR-Style Consent
India's DPDP Act requires notice and consent differently from GDPR. Here is what a compliant Indian cookie banner or consent surface looks like, and where GDPR patterns mislead.
Consent Receipts: Why Proof of Consent Is the Real Compliance Asset
A consent receipt records what a user saw, what they chose, and when, in a form that survives configuration changes. Under DPDP, the receipt is the difference between a banner and a defense.