Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Consent

Append-only consent ledger (GDPR Art. 6/7) — record, withdraw, and gate consent per subject + purpose as a Bijection component

Bijection source
git clone https://github.com/bijectionhq/consent.git
cd consent
git checkout 3ec34d269c378bf4106a91e133917a94df23eb52

Bijection 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.

Available as a reference

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. 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.

agent-prompt.md
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 — 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<TProof> 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

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

MethodKindResult
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?)queryConsentCheck ({ granted; stale; decision; version; at })
getState(ctx, subjectRef, purpose)queryConsentState | null
getStatesForSubject(ctx, subjectRef)queryConsentState[]
history(ctx, subjectRef, purpose, paginationOpts)queryPaginationResult<ConsentEvent>
prune(ctx, opts?)mutationnumber (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 at comes from Date.now() in the handler, never the caller; the proof is 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.

Keywords