Consent
Append-only consent ledger (GDPR Art. 6/7) — record, withdraw, and gate consent per subject + purpose as a Bijection component
git clone https://github.com/bijectionhq/consent.git
cd consent
git checkout 3ec34d269c378bf4106a91e133917a94df23eb52Bijection repository access is required. Request access.
Description
Append-only consent ledger (GDPR Art. 6/7) — record, withdraw, and gate consent per subject + purpose as a Bijection component
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/consent. Based on vllnt/convex-consent. License: MIT. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Append-only consent ledger (GDPR Art. 6/7) — record, withdraw, and gate consent per subject + purpose as a Convex component
Explore the demo ↗Upstream overview
From the upstream package documentation.
@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 at VLLNT. Find the author on GitHub and X.
Use cases
From the upstream package documentation.
- 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
From the upstream package documentation.
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 package FAQ
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.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Consent with Bijection.
Read: https://bijection.com/components/consent/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from vllnt/convex-consent, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@vllnt/convex-consent
An append-only consent ledger (GDPR Art. 6/7), as a Convex component.
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 —
recordappends an immutable event and updates the projection;checkis the O(1) runtime gate before processing. - Append-only ledger — every decision is a
consentEventsrow, never mutated;historypages 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 —
checkreturnsstale: truewhen a grant is for an older policy version thanrequiredVersion. - Server-sourced time — every event's
atis stamped from the server clock; a caller can never supply a timestamp. - Typed, opaque proof —
Consent<TProof>with an optionalproofValidatornarrows 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.usemounts (web + marketing consent on one backend), each an isolated sandbox.
Installation
pnpm add @vllnt/convex-consent
Peer dependency: convex@^1.41.0.
Usage
// 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;
// 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<ConsentEvent> |
prune(ctx, opts?) | mutation | number (ledger events removed in the first bounded pass) |
Full reference: 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
atcomes fromDate.now()in the handler, never the caller; theproofis opaque, narrowed by the host validator.
See docs/API.md.
Testing
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.
Author
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
License
MIT — see LICENSE.