GDPR Article 7(3) states that a data subject has the right to withdraw consent at any time and that withdrawal must be as easy as giving consent — a global unsubscribe that silently leaves other campaign lists active violates this requirement. ePrivacy Article 5(3) applies separately to marketing communications. CCPA §1798.120 gives consumers the right to opt out of all sale and sharing, not selected categories. Multi-list email platforms (Mailchimp, Klaviyo, HubSpot) default to list-scoped unsubscribes, which means a contact who clicks the standard unsubscribe link in a newsletter will continue receiving promotional and partner emails unless the sending path explicitly checks for global suppression first.
High because a global opt-out that only suppresses one campaign type means the system continues sending to a contact who has exercised their GDPR Art. 7(3) or CCPA §1798.120 withdrawal right, creating direct regulatory liability.
Add a global suppression guard as the first check in every send path, before any scope-specific consent lookup:
// src/lib/campaigns/can-send.ts
async function canSendCampaign(
contactId: string,
scope: string
): Promise<boolean> {
// Global suppression always overrides scope consent
const globalSuppression = await db
.selectFrom('opt_outs')
.select('id')
.where('contact_id', '=', contactId)
.where('scope', '=', 'global')
.where('active', '=', true)
.executeTakeFirst()
if (globalSuppression) return false
return hasConsent(contactId, scope)
}
When a contact triggers a global unsubscribe (via List-Unsubscribe header or "unsubscribe from all" action), insert a revocation record for every active scope, plus one with scope = 'global'. Verify that every send path in src/ calls canSendCampaign() — grep for any path that calls hasConsent() directly without the global guard.
ID: compliance-consent-engine.opt-out-processing.global-unsubscribe
Severity: high
What to look for: When a contact opts out globally (via the standard list-unsubscribe header or a "unsubscribe from all" action), check that the suppression applies across all campaign types — not just the specific list or campaign they unsubscribed from. This is a common failure for multi-list email platforms where unsubscribing from "newsletter" does not suppress "promotions" sends. Look at the opt-out processing logic to verify it inserts a revocation record for every scope, or sets a global suppression flag that the sending path checks before any scope.
Pass criteria: A global unsubscribe creates suppression records for all active consent scopes (or sets a globally_suppressed flag). The sending path checks global suppression before checking scope-specific consent. A contact with a global opt-out cannot be sent any campaign type. Enumerate all send paths and count how many check global suppression first — at least 100% must.
Fail criteria: A global opt-out only suppresses the specific list or campaign type it was triggered from. Other campaign types can still send to the contact. Or the sending path checks scope-specific consent without first checking global suppression. Checking scope consent without a global suppression guard does not count as pass.
Skip (N/A) when: No email sending, or the application only has one communication type with no scope differentiation.
Detail on fail: "Unsubscribe from newsletter suppresses only scope=newsletter — scope=promotions sends still reach the contact" or "Global suppression flag exists but CampaignService.canSend() does not check it before checking scope consent"
Remediation: Check global suppression first in the sending path:
async function canSendCampaign(contactId: string, scope: string): Promise<boolean> {
// 1. Check global suppression first — always overrides scope consent
const globalSuppression = await db
.selectFrom('opt_outs')
.select('id')
.where('contact_id', '=', contactId)
.where('scope', '=', 'global')
.where('active', '=', true)
.executeTakeFirst()
if (globalSuppression) return false
// 2. Check scope-specific consent
return hasConsent(contactId, scope)
}