PII never leaves your pipeline without consent.
Period.
Why is your CAPI sending hashed-but-not-consented PII to Meta?
Most CAPI tools — Stape, server-side GTM, custom Worker setups — hash em and ph before forwarding. That's the easy part. The hard part nobody implements is gating the send on actual consent state. A SHA-256 hash of an unconsented email is still personal data under GDPR Art. 4(1) and still triggers Schrems II concerns when it lands in a US ad platform.
The CNIL fined a French retailer €600,000in 2024 for exactly this pattern: a CMP that captured opt-out, an ad pixel that ignored it, and server-side hashing the DPO believed was “anonymous.” The Irish DPC has open inquiries into eight large e-commerce brands on the same hashed-CAPI angle. The EDPB's 2024 guidance on tracker-based marketing is explicit: hashing is not anonymization.
Your DPO can't prove compliance from a Stape dashboard. There's no per-event log of what was sent, what was blocked, and which consent string was active at send time. When the regulator asks for audit evidence covering Q1, you need a CSV export you can hand to legal — not a screenshot of a GTM container.
applyFirewall runs before every send.
Event arrives
Consumer Worker dequeues a normalized event from the events queue. Event already carries consent_state (ad_storage, ad_user_data, analytics_storage, ad_personalization).
Firewall reads policy + consent
applyFirewall loads the merchant's active policies (default + custom), parses the consent_state, and resolves region from event.geo.country.
Evaluates rules in priority order
Custom rules (Pro+) evaluate first; default rules apply last as fallback. First matching block-rule short-circuits. modify-rules accumulate.
Returns action + sanitized event
{ action: 'send' | 'block' | 'modified', sanitized_event, log_entry }. The Worker only calls platform.sendEvent() when action !== 'block'.
Logs persist 30 days rolling
log_entry written to firewall_logs (event_id, platform, action, rule_matched, consent_state, sanitized_fields, region, ts). Exportable as CSV for the legal team.
export interface FirewallResult {
action: 'send' | 'block' | 'modified';
sanitized_event: NormalizedEvent;
log_entry: FirewallLogEntry;
}
export function applyFirewall(
event: NormalizedEvent,
platform: PlatformId,
policies: ConsentPolicy[],
consent_state: ConsentState,
): FirewallResult {
const rules = compilePolicies(policies);
for (const rule of rules) {
if (rule.matches(event, platform, consent_state)) {
if (rule.action === 'block') {
return blockResult(event, platform, rule);
}
if (rule.action === 'modify') {
event = rule.apply(event);
}
}
}
return sendResult(event, platform);
}
// Sample evaluation
const r = applyFirewall(
{ event_name: 'Purchase', user_data: { em: 'a@b.co' }, geo: { country: 'DE' } },
'meta',
defaultPolicies,
{ ad_storage: 'denied', ad_user_data: 'denied' },
);
// → { action: 'block',
// log_entry: { rule_matched: 'region=EU and consent.ad_storage=denied' } }Default at Starter, custom DSL at Pro.
Safe defaults, zero config.
Auto-applied to every merchant on Starter and above. Three rules cover ~90% of GDPR/CCPA exposure for a typical DTC store. You don't see the editor; you see firewall_logs proving it's working.
- ▸Redact PII (em, ph, ln, fn, zp) when consent.ad_storage = denied
- ▸Hash all user_data fields with SHA-256 before any platform send
- ▸Block sensitive event_names matching ^health_, ^pregnancy_, ^fertility_
Write rules per-region, per-platform, per-event.
Full grammar: when ⟨condition⟩ then ⟨action⟩. Conditions on consent state, region, platform, event name (regex), and user_data fields. Actions: block, redact(...), hash(...).
- ▸Per-region overrides (EU / UK / US-CA / US / APAC)
- ▸Platform-scoped hashing (e.g. always hash phone for Meta only)
- ▸Regex event-name blocking for sensitive verticals
- ▸Live syntax check + dry-run against last 1k events
What it looks like in TrackLayer.
CSV export your DPO will actually use.
timestamp,event_id,platform,action,policy_rule_matched,consent_state,sanitized_fields,region 2026-01-04T14:02:11Z,evt_8aJ2,meta,send,default·hash_before_send,"granted/granted/granted/granted",em→sha256;ph→sha256,DE 2026-01-04T14:02:14Z,evt_8aJ3,google,block,"region=EU and consent.ad_storage=denied","denied/denied/granted/denied",,DE 2026-01-04T14:02:17Z,evt_8aJ4,tiktok,modified,default·redact_when_denied,"denied/denied/granted/denied","removed: em,ph",FR 2026-01-04T14:02:20Z,evt_8aJ5,meta,block,"event_name~/^pregnancy_/","granted/granted/granted/granted",,NL 2026-01-04T14:02:23Z,evt_8aJ6,pinterest,send,default·hash_before_send,"granted/denied/granted/granted",em→sha256,IE
?from=2026-01-01&to=2026-03-31
Returns up to 90 days per call (firewall_logs retention). API-key authed; rate-limited at 4 calls/min so a junior engineer can't accidentally DoS your own audit. Streams a gzipped CSV for batches over 100k rows. Same payload is reachable from the dashboard download button.
Stape and Elevar still don't gate on consent.
- Consent gating before send (block on denied)
- •
- Policy DSL (when/then rules)
- •
- Audit log retention (per-event, exportable)
- 30d rolling
- CSV export for DPO (full schema)
- •
- Per-region policy overrides
- •
- Sensitive event_name blocking (regex)
- •
- Default safe-policy on signup
- •
- Consent gating before send (block on denied)
- —
- Policy DSL (when/then rules)
- —
- Audit log retention (per-event, exportable)
- —
- CSV export for DPO (full schema)
- —
- Per-region policy overrides
- —
- Sensitive event_name blocking (regex)
- —
- Default safe-policy on signup
- —
- Consent gating before send (block on denied)
- ~
- Policy DSL (when/then rules)
- —
- Audit log retention (per-event, exportable)
- 7d UI only
- CSV export for DPO (full schema)
- —
- Per-region policy overrides
- —
- Sensitive event_name blocking (regex)
- —
- Default safe-policy on signup
- ~
Three teams, three Friday afternoons saved.
EU DTC store passing GDPR audit
Berlin-based skincare brand, €4M ARR. CNIL-style audit triggered by a customer complaint. Exported 90 days of firewall_logs as CSV in 30 seconds, handed to legal counsel. Showed every consent-denied event was blocked from Meta CAPI. Audit closed in 11 days with zero remediation.
Agency managing 12 stores, 6 jurisdictions
Stockholm performance agency with merchants in DE, FR, ES, UK, US, AU. One policy template, six per-region overrides. The agency rolled out the same DSL to all 12 stores in a morning — UK gets ICO-style strict consent, US gets CCPA do-not-sell, AU gets the lighter Privacy Act profile. One audit log per merchant, isolated.
SaaS pre-empting CCPA enforcement
LA-based subscription brand reading the new CPPA enforcement memo. Added one rule: when region=US-CA and consent.ad_personalization=denied then block. Took two minutes. Caught 8% of California events that the previous tag-manager setup was leaking. Three months of clean logs before the first regulatory advisory in the vertical.
Starter onwards.
Default firewall: redact PII when consent denied, hash before send, block sensitive event names. Auto-applied. firewall_logs retained 30 days rolling. CSV export included.
Start trial →Everything in Starter plus the policy DSL editor. Custom per-region, per-platform, per-event rules. Live syntax check + dry-run against the last 1k events. Multi-policy support.
See Pro plan →Custom regional policy templates, dedicated DPA support, extended firewall_logs retention (up to 365 days), private compile-once-deploy-everywhere policy bundles for agency portfolios.
Talk to sales →