Module

Consent

An append-only record of every grant and withdrawal, in your own D1. Purposes declared and versioned, signed consent kept with its exact document, visitors merged into their account at sign-in, and one question every capability asks before it processes under consent.

@pithy-sh/consent

terminal
pithy add consent

What it is for

Demonstrating consent where it is the legal basis, as GDPR Article 7 asks: specific to a purpose, informed by a particular notice, as easy to withdraw as to give, and recorded. Each purpose has a notice version, and bumping it makes every older grant answer no until the person agrees again.

Signed consent, where a checkbox is not enough. A purpose can demand a typed name, a drawn signature or an e-signature provider's completed envelope, and a grant below that is refused. The grant keeps the exact document by its hash, produced again byte for byte, with how identity was verified, an expiry, and a parent or guardian signing for someone else. A purpose can also require a withdrawal at the grant's own grade, as a HIPAA revocation must be in writing.

Visitors before they have an account. A visitor consents against an anonymous device id, held in a first-party cookie that is set only once they choose, and merged into their account when they sign in. A phone holds no cookie, so a signed-out app sends a visitor id of consent's own instead, never the device id auth registers.

Email that follows it. With consent composed, marketing mail goes only to a subject who agreed, and opens and clicks are tracked only for one who agreed to that. Transactional mail is never gated, and a withdrawal from marketing suppresses the address for marketing until the person agrees again.

What it sits on

Your D1, with the schema refusing to change it.

Every grant and withdrawal is a row in pithy_consent_records, and database triggers refuse an update of any fact and every delete. So append-only is a property of the schema, not a promise in the code. A signed document is kept once per hash, and a drawn signature or a provider's certificate goes to an R2 bucket under its own hash.

Every row is also an audit event, so with audit composed its hash chain makes the record tamper-evident. The typed and representative names are encrypted at rest.

hasConsent is a seam in core, so a capability asks it without importing this package, and with no consent composed it gets no reader and behaves as it did before.

  • D1
  • R2

The honest bit

It records and gates. It does not decide which law applies.

Whether a purpose needs consent at all, which jurisdiction applies, and whether to refuse to store a classed field without it are your calls. The kit records what was agreed to and gates what you told it to gate.

It ships one screen, the settings screen where a person reads and withdraws their consents. There is no cookie banner component, though a banner is a client of the same two routes.

There is no e-signature provider integration. Your own integration supplies the completed envelope and its certificate, and the kit keeps them. It verifies no age either: a representative is recorded as your flow declares them.

If you build it yourself

A consent checkbox is a boolean. A consent record is not.

Storing that somebody ticked a box is one column. Showing an auditor what they agreed to, when, and that they have not withdrawn since is the work.

A record nothing can rewrite

A boolean on a user row can be flipped, and the flip erases the evidence that it was ever otherwise. The record has to be every grant and every withdrawal, in order, in rows the database refuses to change.

The notice behind each grant

Consent is to a particular notice. When the notice changes, every grant under the old one has to stop counting until the person agrees again, and a stale grant must never pass for a current one.

The document, byte for byte

A signed authorization is only proof if you can produce exactly what was signed. That means keeping the rendered text by its hash, and the signature beside it, for as long as the record lives.

Visitors who become accounts

People choose on a marketing page before they sign up. Their choice has to be recorded against something that is not an account yet, without setting a cookie before they choose, and carried over when they sign in.

One answer every feature asks

Marketing mail, tracking and your own processing each need the same yes or no, decided the same way. Scatter the check and the answers drift apart.

terminal
pithy add consent

In the dashboard

On Enterprise, the evidence pane reads it.

Counts per purpose and notice version with the expired grants among them, the grants that have expired or expire within 30 days, one person's record, and the signed documents behind it are control-plane reads behind consent:read. On Enterprise, the dashboard's evidence pane draws them in its consent section and exports each as your Worker answered it. On any plan, they are there for a management client you write yourself. There is no management write: consent is the person's to give and to take back.

Add it, and get on with the feature.

One command installs the package, wires its bindings into every environment your project declares, and runs its migrations.