Where consent is the legal basis for processing, the controller has to be able to demonstrate it (GDPR Art. 7(1)): specific to a purpose, informed by a particular notice, as easy to withdraw as to give, and recorded. A checkbox that sets a boolean on a user row demonstrates none of that.
And some consent is a signature. An app holding health data collects signed consent to store it: an explicit consent under GDPR Article 9, or a HIPAA authorization, which carries an expiry and can only be revoked in writing. What makes that proof is the exact document signed, how it was signed, by whom, and until when.
@pithy-sh/consent records both, in your own D1.
What it gives you
- Purposes, declared and versioned. A capability that processes under consent declares its own purposes:
emaildeclaresemail.marketingandemail.tracking. You add yours inconsent({ purposes }). Bump a purpose’s notice version and every older grant answers no until the person agrees again. - A purpose says what it demands. The data classes it covers, whether it is explicit, the weakest method a grant may be given by (
click,typed,drawnorprovider), and whether a withdrawal must be written at the grant’s grade. - An append-only record. Every grant and withdrawal is one row the database refuses to update or delete, and one audit event.
- Evidence for a signed grant. The exact document by its SHA-256, produced again byte for byte. The typed name, the drawn signature or the provider’s envelope. How identity was verified, an expiry, and a representative signing for someone else. The names rest encrypted.
- Visitors and accounts. A visitor consents against an anonymous device id, in a cookie in a browser or a header of consent’s own on a phone, merged into their account when they sign in.
- One question.
hasConsent(subject, purpose)is what every capability asks before consent-based processing. - Email follows it. Marketing mail and open and click tracking go only to a subject who agreed. Transactional mail is never gated.
- A settings screen.
pithy ui add react --consentandpithy ui add react-native --consentscaffold the screen where a person reads and withdraws their own consents. - A management view. A management client reads counts per purpose and notice version with the expired grants among them, the grants that have expired or expire soon, one subject’s record, and the documents and signatures behind it. It can never grant or withdraw.
The record is append-only by schema
The database refuses to change it. Triggers abort an update of any fact column and every delete, on the records, the documents and the merges. The one write allowed is the field-key rotator re-sealing an encrypted name. So append-only is a property of the schema, not a promise in the code.
Every row is also an audit event. With audit composed, its hash chain makes the record tamper-evident: a row deleted from D1 leaves its event standing in a chain that verifies, and an event deleted breaks the chain.
An account’s purge keeps it. The record is the proof that processing was allowed while it ran and that it stopped. The capability declares retain, with that basis, and an access request exports every row with the names opened.
What hasConsent answers no for
A grant counts only while all of this holds:
- it is under the purpose’s current notice version,
- its method is at or above the purpose’s minimum,
- it has not expired,
- it has not been withdrawn since,
- and, for a visitor never merged into an account, it is inside
visitorWindowDays, 395 by default.
With no consent capability composed, a capability that asks gets null instead of a reader, and behaves as it did before.
What it deliberately does not do
It decides no law. Which jurisdiction applies, whether a purpose needs consent at all, and whether to refuse to write a classed field without it are yours. The kit records and gates.
It ships no cookie banner. The settings screen is the one screen it scaffolds. A banner is a client of the same two routes.
It integrates no e-signature provider. Your integration supplies the completed envelope and its certificate, and the kit keeps them.
It verifies no age. A representative is recorded as your flow declares them.
When you would reach for it
When consent is the legal basis for something your app does. Marketing email and tracking are the common case, and the reason email follows it. A health or wellness app that keeps signed authorizations. A product that asks visitors before it sets anything, and has to show later who agreed to what.
What it needs
auth is a peer. Both writes wear the same-origin gate auth publishes, except a signed-out phone’s cookie-less request, and are held to auth’s AUTH_RATE_LIMITER by default. email, audit and controlplane are optional: email declares its purposes and gates on them, audit makes the record tamper-evident, and the control plane carries the management reads.
pithy add consent writes the config and the bindings. Using Consent has the calls, and the reference has every route, code and table.