Source of truth
The canonical store for consent decisions is the consent record table — one row per(tenantId, subjectId, purpose) with a status of
granted, revoked, or pending.
Read it via the async resolver:
getConsent reads ConsentRecord rows first. If the customer has any rows,
those become authoritative and the attribute fallback is ignored. If
none exist, it falls back to the legacy customer-attribute keys
(consent_marketing, consent_email, etc.) for backward compatibility.
To disable the attribute fallback (recommended after a tenant has run
the backfill script — see below), pass { recordCanonical: true } as
the fourth argument:
Enforcement at decision time
Consent is enforced in the Recommend decision path. At inventory load the recommend pipeline resolves consent once withgetConsent(tenantId, customerId, attributes) and suppresses any candidate whose required
channel consent is revoked. Because it runs once at inventory load,
it applies regardless of which downstream filter nodes a flow contains.
Each candidate’s channel type is mapped to a required consent key before
the check:
| Channel type | Required consent key |
|---|---|
email | email |
sms | sms |
push | push |
phone | phone |
direct_mail, display, in_app, web | marketing |
Consent enforcement is fail-open by design:
- A customer with no ConsentRecord is treated as consented — they
pass (
getConsentreturns the all-permissive default). - A candidate on a channel type not in the map above is allowed (unknown channel types are not suppressed).
- If consent resolution itself fails (for example a DB error), the request logs a warning and keeps all candidates rather than dropping them.
revoked (or non-granted) ConsentRecord for the
channel’s required consent key suppresses a candidate.afterConsent.
This consent stage is also where the do_not_contact
rule’s external dncSource intent is enforced — the per-candidate
contact-policy engine does not perform its own external suppression-list
lookup.
Purposes recognized
| Consent record purpose | Maps to ConsentStatus key |
|---|---|
marketing | marketing |
email | email |
sms | sms |
push | push |
phone | phone |
third_party | thirdParty |
getConsent.
Add a mapping in the platform consent helper module if you need a new
key.
DSAR / GDPR Article 7 compliance
Every consent change is recorded withgrantedAt / revokedAt timestamps
and a source field (manual, api, import, backfill). This
satisfies Article 7’s “demonstrable consent” requirement: at any point
the controller can show the exact moment consent was granted, by what
channel, and on what source.
DSAR exports include the full ConsentRecord trail for the subject (see
DSAR portability).
Backfill from legacy attributes
If your tenant currently stores consent in customer attributes (e.g.consent_marketing: true), run the backfill once to populate
ConsentRecord rows:
{ recordCanonical: true } to getConsent.
Deprecated: synchronous attribute extraction
The legacy synchronous attribute-based consent helper is preserved for backward compatibility only. New callers must use the asyncgetConsent instead.
The deprecated path is wire-flagged: it’ll be removed in a future
release once all production callers migrate. Track the deprecation
status via the scaffold-coverage audit script.
What’s tested
23 unit tests cover the consent surface:- 17 cover the deprecated extract / has / filter helper paths (kept for backward compat).
- 6 cover the new async
getConsentpath: consent-record-first read, attribute fallback,recordCanonicalflag, third-party mapping, and DB-unavailable graceful degradation.
What ships with this surface
- Platform consent helper — async
getConsent(canonical) plus the deprecated attribute helpers. - Consent-helper test suite — full coverage of canonical and legacy paths.
- Consent-record backfill script — one-time migration for tenants moving off legacy attribute storage.
- The consent record Prisma model.