# Consent Append-only consent ledger (GDPR Art. 6/7) — record, withdraw, and gate consent per subject + purpose as a Bijection component - Package: @bijectionhq/consent - Category: Database & Backend - Status: Reference - License: MIT - Bijection source (repository access required): https://github.com/bijectionhq/consent/tree/3ec34d269c378bf4106a91e133917a94df23eb52 - Upstream: https://github.com/vllnt/convex-consent/tree/591da416d12a343c791141a899e7338f3d9d347b - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/consent ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/consent.git cd consent git checkout 3ec34d269c378bf4106a91e133917a94df23eb52 ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes @vllnt/convex-consent; check the Bijection adaptation separately. @vllnt/convex-consent stores consent decisions by subject and purpose, with a current-state view and a retained event history. Your application defines each purpose, policy version, and proof format. record appends a decision and updates current state. check reports whether consent is granted and whether its version meets the required version. withdraw records withdrawal, while history exposes prior events with server timestamps. Retention controls remove eligible superseded events. The host owns identity, access control, notices, consent collection, and processing behavior. A version mismatch does not display a prompt automatically; your app handles it. This component supports consent records and checks, not a guarantee that a product meets legal obligations. Maintained by [bntvllnt](https://bntvllnt.com) at [VLLNT](https://vllnt.com). Find the author on [GitHub](https://github.com/bntvllnt) and [X](https://x.com/bntvllnt). ### Use cases - Mobile apps: record a person’s analytics or personalization choice. - Web products: check the current decision before optional data processing. - Communications: retain a consent decision and its supporting evidence. - Policy updates: detect older grants and present an updated choice in your own UI. ### How it works record appends a decision and updates current state. check reports whether consent is granted and whether its version meets the required version. withdraw records withdrawal, while history exposes prior events with server timestamps. Retention controls remove eligible superseded events. The host owns identity, access control, notices, consent collection, and processing behavior. A version mismatch does not display a prompt automatically; your app handles it. This component supports consent records and checks, not a guarantee that a product meets legal obligations. ### Upstream README # @vllnt/convex-consent An append-only consent ledger (GDPR Art. 6/7), as a Convex component. ```ts const consent = new Consent(components.consent); await consent.record(ctx, subjectRef, "analytics", "granted"); const gate = await consent.check(ctx, subjectRef, "analytics"); // { granted, stale, ... } ``` The host records a subject's decision for a purpose (`granted` / `denied` / `withdrawn`) — each call appends an immutable ledger event (the legal proof) and updates an O(1) current-state projection — then gates processing with `check`. When the policy version changes, a prior grant goes `stale` so the host re-prompts. Domain-neutral: a cookie gate, a marketing opt-in, a device-tied consent. ## Features - **Record-and-gate** — `record` appends an immutable event and updates the projection; `check` is the O(1) runtime gate before processing. - **Append-only ledger** — every decision is a `consentEvents` row, never mutated; `history` pages the full trail for an audit or DSR. - **Easy withdrawal** — `withdraw` (Art. 7(3)) revokes a held grant; withdrawing something never granted throws a coded error. - **Version staleness** — `check` returns `stale: true` when a grant is for an older policy version than `requiredVersion`. - **Server-sourced time** — every event's `at` is stamped from the server clock; a caller can never supply a timestamp. - **Typed, opaque proof** — `Consent` with an optional `proofValidator` narrows the stored evidence at the boundary. - **Bounded prune + cron** — a daily cron sweeps superseded ledger events past retention; the live projection is never pruned. - **Mount-safe** — correct under multiple named `app.use` mounts (web + marketing consent on one backend), each an isolated sandbox. ## Installation ```bash pnpm add @vllnt/convex-consent ``` Peer dependency: `convex@^1.41.0`. ## Usage ```ts // convex/convex.config.ts import { defineApp } from "convex/server"; import consent from "@vllnt/convex-consent/convex.config"; const app = defineApp(); app.use(consent); export default app; ``` ```ts // convex/consent.ts — host owns auth; pass an opaque subjectRef in. import { components } from "./_generated/api"; import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; import { Consent } from "@vllnt/convex-consent"; const consent = new Consent<{ policyHash: string }>(components.consent, { defaultVersion: "2024-policy", proofValidator: v.object({ policyHash: v.string() }).parse, }); // Record a decision, gate processing on it, and withdraw — host resolves identity → subjectRef. export const setConsent = mutation({ args: { purpose: v.string(), granted: v.boolean(), policyHash: v.string() }, handler: async (ctx, { purpose, granted, policyHash }) => { const subjectRef = await resolveSubject(ctx); await consent.record(ctx, subjectRef, purpose, granted ? "granted" : "denied", { proof: { policyHash } }); }, }); export const mayProcess = query({ args: { purpose: v.string() }, handler: async (ctx, { purpose }) => { const subjectRef = await resolveSubject(ctx); const gate = await consent.check(ctx, subjectRef, purpose, "2024-policy"); return gate.granted; // false (gate.stale === true) if they consented to an older policy }, }); export const revoke = mutation({ args: { purpose: v.string() }, handler: async (ctx, { purpose }) => consent.withdraw(ctx, await resolveSubject(ctx), purpose), }); ``` Client options: `new Consent(component, { defaultVersion?, proofValidator? })`. ## API Reference | Method | Kind | Result | |--------|------|--------| | `record(ctx, subjectRef, purpose, decision, opts?)` | mutation | `{ at }` (`decision`: `"granted" \| "denied" \| "withdrawn"`; `opts`: `{ version?; proof? }`) | | `withdraw(ctx, subjectRef, purpose, proof?)` | mutation | `{ at }` | | `check(ctx, subjectRef, purpose, requiredVersion?)` | query | `ConsentCheck` (`{ granted; stale; decision; version; at }`) | | `getState(ctx, subjectRef, purpose)` | query | `ConsentState \| null` | | `getStatesForSubject(ctx, subjectRef)` | query | `ConsentState[]` | | `history(ctx, subjectRef, purpose, paginationOpts)` | query | `PaginationResult` | | `prune(ctx, opts?)` | mutation | `number` (ledger events removed in the first bounded pass) | Full reference: [docs/API.md](docs/API.md). ## React Backend-only — no `./react` entry. Consent state is read through an ordinary reactive `useQuery` over the host's re-exported `check` / `getState` refs; consent proof is sensitive and gating is server-side, so there is no safe client surface. ## Security - **Auth-agnostic** — the host authenticates the caller, decides who may record/read a subject's consent, and passes an opaque `subjectRef`; tables are sandboxed. - **Append-only** — a recorded decision and its proof can never be silently rewritten; withdrawal is a new event, not an edit. - **Server-sourced time** — every event's `at` comes from `Date.now()` in the handler, never the caller; the `proof` is opaque, narrowed by the host validator. See [docs/API.md](docs/API.md). ## Testing ```bash pnpm test # single run pnpm test:coverage # enforced 100% on covered files ``` Tests run against the real component runtime via `convex-test` (`@edge-runtime/vm`), not mocks. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). ## Author Built by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt) Part of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com) If this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt). ## License MIT — see [LICENSE](LICENSE).