Hash-keyed do-not-contact suppression list and opt-in proof ledger for Convex with channel-aware tombstones, GDPR/CAN-SPAM compliance, and a single isEligible s
npm install @vllnt/convex-suppressionSuppression List is a do-not-contact (DNC) list plus opt-in proof ledger as a Convex component: register a `contactHash` tombstone by channel or globally, gate every outbound send through `isEligible`, and store legal consent evidence via `recordOptIn` — all without touching raw PII. Records are keyed by an opaque `contactHash` the host produces, so a suppression outlives subject erasure and satisfies GDPR / CAN-SPAM primitives at the data layer. Suppression entries, opt-in proofs, and server-sourced timestamps live in sandboxed component tables; the host gates every write, and it runs identically on Convex Cloud and self-hosted convex-backend.
The @vllnt/convex-suppression component provides a hash-keyed suppression list that stores opaque contactHash values instead of raw PII. Call suppress() on unsubscribe, bounce, or complaint events, then call isEligible() before every send to check whether the contact is eligible. The component is channel-aware, so you can suppress a contact for email independently of sms or push.
The @vllnt/convex-suppression component is designed for GDPR and CAN-SPAM compliance by storing only an opaque contactHash rather than raw PII, so suppression tombstones survive erasure of the underlying subject data. The recordOptIn mutation stores legal proof-of-consent per list, and getOptInProof retrieves it. Server-sourced timestamps prevent callers from supplying falsified confirmation times.
Use the recordOptIn mutation from @vllnt/convex-suppression to record consent at confirmation time, passing a listKey, source string, and optional typed proof object validated by a host-supplied proofValidator. Retrieve the stored record later with getOptInProof. The isEligible query accepts a requireOptIn flag so sends are blocked unless confirmed consent exists for that list.
The suppress mutation in @vllnt/convex-suppression is idempotent: re-suppressing on the same contactHash and channel tuple updates in place rather than inserting a duplicate row. This means a replayed provider webhook for a bounce or complaint will not create multiple suppression records, making it safe to use directly as a webhook handler without deduplication logic in the host.
No. The @vllnt/convex-suppression component never stores raw PII. The host application is responsible for hashing and normalizing the contact identifier and passing an opaque contactHash string. The component only stores that hash, so suppression tombstones survive a GDPR erasure request that deletes the underlying contact record.
Call isEligible(ctx, contactHash, opts) from a Convex query before every send. The opts object accepts an optional channel to scope the check, an optional listKey to target a specific opt-in list, and an optional requireOptIn boolean that blocks sends unless a confirmed opt-in record exists. The method returns a boolean and handles both suppression and opt-in checks in a single call.
Yes. The component supports multiple named mounts via Convex's app.use, so you can isolate marketing and transactional suppression lists in separate sandboxes. Within a single mount, channel scoping lets you suppress a contact for email independently of sms or push. The listKey parameter on recordOptIn and isEligible scopes opt-in proofs to a specific list.
Yes. The suppress and recordOptIn mutations in @vllnt/convex-suppression are idempotent. Re-calling either on the same key tuple updates the existing row rather than inserting a duplicate, so replayed webhooks from email providers reporting bounces or complaints do not produce duplicate suppression records.
The @vllnt/convex-suppression component requires convex@^1.41.0 as a peer dependency. Install it with npm install @vllnt/convex-suppression, then register the component in your convex/convex.config.ts file using app.use(suppression) imported from @vllnt/convex-suppression/convex.config.