Data model
The objects PIEDAP Consent stores, how projects, properties, forms, keys, records and requests connect, and what the database enforces.
The objects the product stores, how they point at each other, and which links the database enforces. Key ideas explains the same objects in plain language.
The hierarchy#
Organisation, account and project come from the PIEDAP IAM service. The first level this product owns is the property: one site or app.
Project (from IAM)
├── Shared by every property
│ ├── Purposes why data is used; reused by forms
│ ├── Policies privacy notices a form links to, in versions
│ ├── Project settings one row per project
│ ├── Webhook endpoints with an outbox and a delivery log
│ ├── Rights requests by person
│ ├── Breach register
│ ├── Evidence anchors one per day with sealed events
│ ├── Partner register what each partner is to you
│ ├── Erased hashes identifiers of erased people
│ └── Audit log append-only
├── Personal dashboards each person's own Overview charts
└── Property a site or app, recognised by its origins
├── Consent forms what is asked
│ ├── Versions frozen when published
│ └── Form purposes which purposes the form offers, in order
├── Region rules which form each place sees
├── Experiments two forms of the property compared
├── Keys tied to the property, or to the whole project
├── Scans and cookies what was found on the property
├── Consent imports one per file imported
├── Notice campaigns a notice version sent to a list
│ ├── Uploads, people and their identifiers
│ ├── Batches and links
│ └── Campaign anchors, one per day with answers
└── Consent records one per person per form
├── Events every state, sealed in a chain
├── Linked identifiers
├── Email confirmations
└── Processor tasks, with their reportsRows of configuration, records and requests carry the organisation, account and project ids. Three tables differ:
- the audit log may leave them empty
- a leaf tombstone carries only the project
- an idempotency record carries the project and the key
The objects#
| Object | Table | Belongs to | What it is |
|---|---|---|---|
| Property | property |
Project | A website, mobile app, connected-TV app or server integration, with its origins |
| Purpose | purpose |
Project | One reason for using data: key, category, legal basis, data used, partners |
| Consent form | consent_form |
Property | The draft of a notice: wording, layout, settings, languages |
| Form version | consent_form_version |
Form | A published copy, frozen, with a SHA-256 of its exact definition |
| Form purpose | consent_form_purpose |
Form and purpose | That a form offers a purpose, and in which position |
| Policy | policy_document, policy_document_version |
Project | A privacy notice a form links to, in published versions |
| Region rule | region_rule |
Property, points at a form | Which form a continent, country or subdivision sees, and under which law and consent model |
| Experiment | form_experiment |
Property | A comparison of two forms of the same property |
| Key | sdk_key |
Project, optionally one property | A publishable key, an optional secret key, allowed origins, scopes, and live or test |
| Consent record | consent_record |
Property, form and form version | One person's answer to one form as it stands, with any basis, notice and capture context sent |
| Consent event | consent_event |
Record | One state of a record, with its seal. An export or an import adds an event that holds no state and sits outside the chain |
| Linked identifier | consent_subject_link |
Record | Another identifier the record can be found by: type and keyed hash |
| Email confirmation | email_verification |
Record | A pending or confirmed double opt-in |
| Rights request | subject_request |
Project | A request from a person, joined to records by the keyed hash of the identifier |
| Evidence anchor | evidence_anchor |
Project | The signed Merkle root of one day's sealed events |
| Leaf tombstone | evidence_leaf_tombstone |
Project | The leaf hash of a deleted event, so its day's anchor still verifies |
| Scan and discovered item | scan_run, discovered_item |
Property | A site scan, and each cookie, storage key or host found |
| Webhook | webhook_endpoint, webhook_outbox, webhook_delivery |
Project | An endpoint, events waiting to be sent, and the delivery log |
| Daily statistics | consent_daily_stat, consent_impression_daily |
Form | Counts per day, form, country and environment, without identifiers. Banner views are also counted per form version |
| Project settings | project_setting |
Project | Contact, deadlines, retention and similar settings |
| Breach | breach_incident |
Project | One entry of the breach register |
| Audit log | audit_log |
Project | Actions by your team and by secret keys. Append-only: a trigger refuses changes and deletions |
| Consent import | consent_import |
Property, form and form version | One file imported from Records: its browser-reported SHA-256, mapping, stated basis and notice, and counts |
| Notice campaign | notice_campaign, notice_campaign_upload |
Property and form version | One published notice version sent to a list, and each file added to it |
| Campaign person | notice_campaign_recipient, notice_campaign_identifier |
Campaign | One person: hashed identifiers, your encrypted reference, language, basis, reported delivery and answer |
| Batch and link | notice_campaign_batch, notice_campaign_link |
Campaign | Up to 100,000 single-use links and their Merkle root. Tokens are stored as HMACs |
| Campaign anchor | notice_campaign_anchor |
Project | The signed root of one day's notice answers and batches |
| Partner register | partner_register |
Project | A partner's role, contact, days to stop and to erase, DPA date and country |
| Processor task | processor_task, processor_task_acknowledgement |
Record | An identifier-free instruction to one processor to stop or erase, and its append-only reports |
| Erased hash | erased_subject_hash |
Project | Every hash an erased record was known by, with the latest erasure. Kept so an import or a campaign never adds the person back |
| Personal dashboard | user_dashboard |
Project and one signed-in person | One person's arrangement of Overview charts: a name, its place in their list, the charts as JSON and a revision number. A display setting, not evidence |
| Idempotency record | idempotency_record |
Project and key | A stored response for a repeated write, kept 24 hours |
How they connect#
One object on the left has many on the right, unless the note says otherwise.
| From | To | Notes |
|---|---|---|
| Property | Consent forms | A form belongs to one property for its whole life |
| Property | Region rules | A rule points at one form of the same property |
| Property | Keys | A key may also belong to the whole project |
| Property | Scans, discovered items, experiments | A scan may also belong to no property |
| Property | Consent records | The record takes the property of its form |
| Consent form | Versions | Publishing adds a version; versions never change |
| Consent form | Form purposes | Each links to one purpose of the project |
| Purpose | Form purposes | A purpose can be offered by many forms |
| Policy | Consent forms | Optional link from a form to a policy |
| Consent form | Region rules | One form can be shown in many places; a place shows one form |
| Form version | Consent records | A record names the exact version the person saw |
| Region rule | Consent records | Optional: the rule that was applied |
| Consent record | Events | Numbered from 1; the sealed ones form the chain |
| Consent record | Linked identifiers | Up to five can be added in one call |
| Consent record | Email confirmations | Deleted with the record |
| Consent record | Processor tasks | Deleted with the record |
| Consent import | Consent records | Optional: the import that wrote the current answer |
| Notice campaign | Uploads, people, batches | Deleted with the campaign |
| Campaign person | Identifiers, links | Up to five identifiers |
| Webhook endpoint | Outbox entries and deliveries | Deleted with the endpoint |
A region rule does not own a form, and a form does not own a region rule. Both belong to the property, and the rule points at one of that property's forms.
From a page view to a sealed record#
- The request carries a key. A key tied to a property reaches only that property.
- Without such a key, a property named in the request is used. Failing that, the request's origin decides, and the most specific match wins.
- The property's region rules are matched against the person's location. The most specific place wins: subdivision, then country, then continent, then the fallback.
- The rule names a form, and its published version is served. A running experiment can serve its other form instead.
- The answer is written as a consent record against the version the person was shown.
- The new state is sealed as an event and chained to the one before.
- After the day closes, the event becomes a leaf of that day's anchor.
Rare cases
- If two properties match an origin equally, none is chosen. A project with one property needs no origins.
- A request can name a form, and the region rules then do not choose it.
- A person may already have a record for the form in the same environment that is not erased. That record is updated.
How consent is proven explains the seal and the anchor.
What the database enforces#
Foreign keys#
Every key inside the hierarchy includes the project id, so a row cannot point at another project's data. Keys that must stay on one site also include the property id. Changing a referenced id is always refused.
| Child | Columns | Parent | When the parent is deleted |
|---|---|---|---|
consent_form |
project, property | property |
Refused |
consent_form |
policy | policy_document |
Link cleared |
consent_form_version |
project, form | consent_form |
Deleted too |
consent_form_purpose |
project, form | consent_form |
Deleted too |
consent_form_purpose |
project, purpose | purpose |
Refused |
policy_document_version |
project, policy | policy_document |
Deleted too |
region_rule |
project, property | property |
Deleted too |
region_rule |
project, property, form | consent_form |
Refused |
form_experiment |
project, property | property |
Refused |
form_experiment |
project, property, control form and variant form | consent_form |
Refused |
sdk_key |
project, property (may be empty) | property |
Refused |
scan_run |
project, property (may be empty) | property |
Deleted too |
discovered_item |
project, property | property |
Deleted too |
consent_record |
project, property | property |
Refused |
consent_record |
project, property, form | consent_form |
Refused |
consent_record |
project, form, form version | consent_form_version |
Refused |
consent_record |
region rule | region_rule |
Link cleared |
consent_event |
project, consent id | consent_record |
Deleted too |
consent_subject_link |
project, record | consent_record |
Deleted too |
email_verification |
project, consent id | consent_record |
Deleted too |
consent_daily_stat |
project, form | consent_form |
Deleted too |
consent_impression_daily |
project, form | consent_form |
Deleted too |
webhook_outbox |
project, endpoint | webhook_endpoint |
Deleted too |
webhook_delivery |
project, endpoint | webhook_endpoint |
Deleted too |
consent_record |
project, import | consent_import |
Refused |
consent_import |
project, property; property, form; form, form version | property, consent_form, consent_form_version |
Deleted too |
notice_campaign |
project, property, form; form, form version | consent_form, consent_form_version |
Refused |
notice_campaign_upload, notice_campaign_recipient, notice_campaign_batch |
project, campaign | notice_campaign |
Deleted too |
notice_campaign_identifier |
project, person | notice_campaign_recipient |
Deleted too |
notice_campaign_link |
project, batch; project, person | notice_campaign_batch, notice_campaign_recipient |
Deleted too |
processor_task |
project, consent id | consent_record |
Deleted too |
processor_task_acknowledgement |
project, task | processor_task |
Deleted too |
Two links use a single column: a form's policy and a record's region rule. Deleting the parent clears the link, and a key that included the project would clear the project id with it. The application checks the project when it writes them.
Uniqueness#
| Table | Unique on | Effect |
|---|---|---|
property |
project, key | Property keys do not repeat in a project |
purpose |
project, key | Purpose keys do not repeat in a project |
policy_document |
project, key | Policy keys do not repeat in a project |
consent_form |
project, property, key | Two properties may each have a form called cookie-banner |
consent_form_version |
form, version | Version numbers do not repeat |
consent_form_purpose |
form, purpose | A purpose is offered once per form |
sdk_key |
publishable key | No two keys share a publishable key |
consent_record |
project, consent id | Consent ids do not repeat in a project |
consent_event |
project, consent id, sequence | Two events cannot claim the same place in a chain |
consent_subject_link |
record, type, hash | An identifier is linked to a record once |
evidence_anchor |
project, day | One anchor per project per day |
discovered_item |
project, property, kind, name | Each property has its own cookie list |
project_setting |
project | One settings row per project |
user_dashboard |
project, person, name | A person's dashboard names do not repeat in a project |
consent_import |
project, while writing | One import writes at a time in a project |
notice_campaign_recipient |
campaign, hash | A person is in a campaign once |
notice_campaign_link |
token hash | Link tokens do not repeat |
notice_campaign_anchor |
project, day | One campaign anchor per project per day |
partner_register |
project, partner key | A partner is registered once |
processor_task |
project, record, partner, action, causing seal | One task per processor for each change |
erased_subject_hash |
project, environment, hash, hash version | Each erased hash once, with its latest erasure |
What follows from these rules#
- A form, version or property that has consent records cannot be deleted. The evidence would lose the wording it points at.
- A consent record always names the property of its form and a version of that same form.
- A region rule can only show a form of its own property.
- An experiment can only compare two forms of the same property.
- A form offers only purposes of its own project, and a purpose that a form offers cannot be deleted.
- Deleting a record removes its events, linked identifiers and email confirmations with it. The retention job keeps a tombstone of each sealed event first.
What the service enforces#
The application checks these rules. No database constraint does.
| Rule | How it shows |
|---|---|
| One record per person per form, per environment | Concurrent first answers are serialised with a database lock, so two records are not created. An erased record is left as it is; a new answer after erasure starts a new record |
| A place is claimed by one region rule of a property | Saving a second rule for the same country is refused with REGION_ALREADY_CLAIMED. Applying a law preset leaves out the places already claimed and saves the rest. A country rule inside a continent rule is allowed; the more specific wins |
| A form never moves to another property | The update call does not accept a new property |
| A consent is written against its form's property | A request that arrived on one property and names another property's form is refused with FORM_NOT_ON_PROPERTY |
| Consent can only be recorded against a published form | Otherwise CONSENT_FORM_NOT_PUBLISHED |
| A published version is not edited | Changes go to the draft; publishing makes a new version |
| Events are not updated or deleted while their record exists | An erasure writes an ERASED event; it does not remove history |
A write with collectedAt never overrules a newer answer, a withdrawal or an erasure |
It is refused with 409 and one of five codes. See Consent |
| Every purpose on the version that can be declined must be answered, and nothing else | Otherwise INVALID_PURPOSE_DECISIONS or INVALID_PARTNER_DECISIONS |
| Scheduled jobs that must not run twice run on one replica at a time | Each takes a PostgreSQL advisory lock |
| A person reaches only their own dashboards | The person and the project come from the session. Another person's dashboard answers 404 |
| A dashboard is saved over only from the version it was read at | A stale revision is refused with 409 and DASHBOARD_CHANGED_ELSEWHERE |
| Up to 10 dashboards per person in a project, each up to 32,768 bytes | Otherwise 409 with DASHBOARD_LIMIT_REACHED, or 400 with DASHBOARD_DEFINITION_TOO_LARGE |
Two exceptions
- The demo data generator sets the time of the events it creates. It is refused in production unless the operator allows it.
- The vendor-list refresh and the clearing of held-back details on erased records run on every replica. Repeating them changes nothing.
Project settings and form settings#
How a banner behaves is set on each form and published with it. The project keeps what applies to the whole organisation. A published version keeps the settings it was published with.
| Setting | Where |
|---|---|
| Privacy button and its position | Form |
| Honour Global Privacy Control | Form |
| Google Consent Mode v2 | Form |
| IAB TCF and Global Privacy Platform | Form |
| Choices per partner | Form |
| Email double opt-in | Form, for email opt-in forms |
| Consent lifetime, languages, wording, layout, purposes | Form |
| IAB CMP id and version | Project |
| Privacy contact, Data Protection Officer, display name | Project |
| Response deadline, evidence retention, inactivity erasure | Project |
| Translation service, subject fingerprint key, what the request form asks for | Project |
| Second person to approve publishing, readable identifier | Project |
People across records#
There is no "person" table. A person is whatever shares an identifier:
- A consent record holds the keyed hash of the identifier it was created under.
- Linked identifiers add more hashes that lead to the same record.
- A rights request holds the hash of the identifier it was filed under. It can be pointed at a different identifier later.
A lookup hashes the identifier you give and returns every record that matches directly or through a link.
- How identifiers are stored: Identifiers
- The lookup call: Find a person