Consent
Art. 7 consent — versioned, timestamped, queryable records.
ConsentLedger
Section titled “ConsentLedger”class ConsentLedger: def __init__(consent_records: Table, audit_sink: AuditSink) -> NoneAppend-only consent bookkeeping (Art. 7).
Withdrawal is as easy as granting — both are one call appending one immutable record. Every call is mirrored into the audit trail.
Consent rows are written through the caller’s session and commit with
it; the mirrored audit event goes through the constructor’s sink, which
persists independently (ADR 0006). A failing sink raises out of
record before the caller can commit, so no consent change can
persist unaudited — the converse (an audit event for a consent write
the caller then rolls back) is possible and is the deliberate, evidence-
preserving direction. Do not commit a session after record
raised.
ConsentLedger.history
Section titled “ConsentLedger.history”def history(session: Session, subject_id: SubjectIdentifier) -> tuple[ConsentRecord, ...]Every consent event for one subject, oldest first.
Args:
- session (
Session): An open database session. - subject_id (
SubjectIdentifier): Whose history to read (single-columnstror compositeCompositeSubjectId).
Returns:
ConsentRecord— The full, unredacted event sequence — this is the Art. 5(2)...— accountability answer.
ConsentLedger.record
Section titled “ConsentLedger.record”def record(session: Session, record: ConsentRecord) -> NoneAppend one consent event (grant or withdrawal).
Mirrors a CONSENT_GRANTED/CONSENT_WITHDRAWN audit event
whose payload carries only the purpose and policy version — never
the record’s source or any other potential PII.
Args:
- session (
Session): An open database session. - record (
ConsentRecord): The event to append. Never updates existing records.
ConsentLedger.status
Section titled “ConsentLedger.status”def status(session: Session, subject_id: SubjectIdentifier, purpose: str) -> boolWhether the subject currently consents to a purpose.
Derived from the latest record for (subject, purpose); False
when no record exists. “Latest” means greatest recorded_at
(supply distinct, caller-clock timestamps). Exact timestamp ties
resolve to the withdrawing record — when the order of a grant and
a withdrawal is unknowable, effaced assumes consent was withdrawn.
Args:
- session (
Session): An open database session. - subject_id (
SubjectIdentifier): Whose consent to check. - purpose (
str): The processing purpose.
Returns:
bool— Current consent status.
ConsentLedger.status_as_of
Section titled “ConsentLedger.status_as_of”def status_as_of(session: Session, subject_id: SubjectIdentifier, purpose: str, at: datetime) -> boolWhether the subject consented to a purpose as of an instant at.
The point-in-time read a DPA actually asks for — “prove what this
subject consented to at time T” — answered from the same append-only
records as status, with no schema change. Considers only
records with recorded_at <= at (the boundary is inclusive: a
record stamped exactly at counts), and applies the identical
“latest wins, withdrawal breaks ties” rule. status_as_of at or
after the subject’s last record equals status.
Args:
- session (
Session): An open database session. - subject_id (
SubjectIdentifier): Whose consent to check. - purpose (
str): The processing purpose. - at (
datetime): The instant to evaluate consent at; compare in the same timezone convention as the storedrecorded_atvalues.
Returns:
bool— Consent status as ofat;Falsewhen no record existsbool— at or before it.
ConsentRecord
Section titled “ConsentRecord”class ConsentRecord(BaseModel): granted: bool policy_version: str = Field(min_length=1, max_length=255) purpose: str = Field(min_length=1, max_length=255) recorded_at: datetime source: str | None = Field(default=None, max_length=255) subject_id: StoredSubjectIdOne consent grant or withdrawal, as it happened.
Records are immutable events, not mutable state: current consent status is derived by reading the latest record per (subject, purpose). This is what makes Art. 5(2) accountability answerable — what consent was given, when, and against which policy version.
Fields:
- granted (
bool):Truefor a grant,Falsefor a withdrawal. - policy_version (
str): Version of the policy text the subject saw. - purpose (
str): The processing purpose consented to (e.g."newsletter"). - recorded_at (
datetime): When the event happened (UTC). - source (
str | None): Where the event came from ("signup_form","api"). - subject_id (
StoredSubjectId): Whose consent this is — a single-columnstror a compositeCompositeSubjectId, stored as its canonical string (ADR 0025).