Append-only GDPR consent ledger for Convex: record, check, and withdraw per-subject decisions with O(1) gating, version staleness, and immutable audit history.
npm install @vllnt/convex-consentConsent 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.
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.
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.
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.
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.
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.
@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.
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.
@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.
@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.