Consent Ledger

Append-only GDPR consent ledger for Convex: record, check, and withdraw per-subject decisions with O(1) gating, version staleness, and immutable audit history.

Installation

npm install @vllnt/convex-consent

About Consent Ledger

Consent Ledger is an append-only GDPR consent store as a Convex component: `record` appends an immutable `consentEvents` row and updates an O(1) projection; `check` returns `{ granted, stale, decision, version, at }` as the runtime gate before any processing. Every event's timestamp is server-sourced — callers cannot supply one — and a grant goes `stale` automatically when the policy version advances, triggering re-prompt without extra bookkeeping. Tables are sandboxed per mount, the host owns auth and passes an opaque `subjectRef`, and the component runs identically on Convex Cloud and self-hosted convex-backend.

Benefits

Use cases

how to implement GDPR consent tracking in Convex

The @vllnt/convex-consent component provides an append-only consent ledger built as a Convex component. You call consent.record(ctx, subjectRef, purpose, 'granted') from a mutation to log a decision, and consent.check(ctx, subjectRef, purpose, requiredVersion) from a query to gate processing. Every decision is stored as an immutable consentEvents row, giving you a legal audit trail without any custom schema work.

how to detect stale consent after a privacy policy update in Convex

When you call consent.check(ctx, subjectRef, purpose, requiredVersion), the component compares the version stored on the subject's current consent state against the requiredVersion you pass in. If the stored version is older, check returns stale: true so you can re-prompt the user before processing. The version is recorded at consent.record time using the version option, and the defaultVersion can be set at instantiation.

append-only audit log for user consent Convex backend

The @vllnt/convex-consent component stores every granted, denied, or withdrawn decision as a consentEvents row that is never updated or deleted during normal operation. The history method returns a paginated PaginationResult of all events for a subject and purpose, suitable for DSR responses or compliance audits. A bounded daily cron prunes only superseded events past a configurable retention window, leaving the live projection intact.

how to handle consent withdrawal GDPR Art 7 Convex

Call consent.withdraw(ctx, subjectRef, purpose) from a mutation to record a withdrawal event for a subject and purpose. This appends a new event with decision 'withdrawn' rather than editing any existing row, satisfying the requirement that the full decision history remains intact. Attempting to withdraw consent that was never granted throws a coded error, so you can distinguish invalid withdrawal attempts from valid ones.

Frequently asked questions

Does @vllnt/convex-consent store timestamps from the client or the server?

Every event's at timestamp in @vllnt/convex-consent is sourced from Date.now() inside the Convex handler, never from the caller. This prevents clients from supplying arbitrary timestamps on consent records, which is important for the legal validity of the audit trail.

How does @vllnt/convex-consent handle authentication and subject identity?

@vllnt/convex-consent is auth-agnostic. The host application is responsible for authenticating the caller and resolving their identity to an opaque subjectRef string, which is then passed into record, check, and withdraw calls. The component's tables are sandboxed and the component never touches auth context directly.

Can I use @vllnt/convex-consent for multiple consent contexts on the same Convex backend?

Yes. Because @vllnt/convex-consent is a Convex component, you can mount it multiple times using named app.use calls in convex.config.ts. Each mount gets its own isolated set of tables, so a cookie consent gate and a marketing opt-in can coexist on the same backend without sharing state.

Does @vllnt/convex-consent include any React client utilities?

@vllnt/convex-consent is backend-only and has no ./react entry point. Consent state is read reactively through the host's own query wrappers using Convex's standard useQuery hook. Gating and proof handling remain server-side, which avoids exposing sensitive consent proof data on the client.

What happens to old consent events over time in @vllnt/convex-consent?

@vllnt/convex-consent schedules a daily cron that runs a bounded prune pass over superseded ledger events that have passed the retention window. The current-state projection is never pruned, so O(1) gating via check is unaffected. You can also trigger pruning manually by calling the prune mutation directly.

Links