GDPR Compliance Checklist for Feature Flags

This how-to is part of Building Audit Trails for Compliance. The most common GDPR failure in flag systems is a consent withdrawal that invalidates the legal basis for targeting but does not flush the cached evaluation context — the user keeps receiving the targeted experience for minutes or hours. This checklist walks through detection, containment, and long-term architectural hardening, in that order.

GDPR compliance layers for feature flag evaluation Four layers — context sanitization, consent gate, audit trail, and retention enforcement — stack between the raw user request and the flag evaluation result. Raw Context email, IP, device PII Sanitizer hash + minimize Consent Gate check before eval Flag Evaluation variant resolved Audit Trail + Retention TTL applies to all stages
GDPR compliance requires sanitization and consent gating before evaluation reaches the rule engine; the audit trail and retention TTL span the full pipeline.

Problem Statement

Feature flag targeting attributes — user ID, email, IP address, device fingerprint, behavioral segment — are personal data under GDPR Article 4. Storing them in evaluation context or audit logs without a lawful basis, hashing them inadequately, or failing to invalidate them after a consent withdrawal are each a compliance gap. The legal consequence is a fine; the operational consequence is a post-incident remediation that touches every log store the data touched.

Targeting attributes are personal data under GDPR Common targeting attributes — user id, email, IP address, device fingerprint, behavioral segment — all fall under GDPR Article 4 as personal data and need a lawful basis and safeguards. targeting attributes = personal data (Art. 4) userId · email · IP · device fingerprint · segment need lawful basis + safeguards gap = fine + remediation across every log store
Anything that identifies a person is in scope; the cost of getting it wrong is a fine plus remediation across every store the raw data ever reached.

Prerequisites

Prerequisites for GDPR-compliant flags Four prerequisites: a configurable evaluation context, a consent management platform emitting events, a stable rotatable HMAC secret, and DPO-approved retention policies. Configurable context OpenFeature Consent platform emits events HMAC secret stable, rotatable DPO-approved retention policy
The consent platform and DPO sign-off are what make the technical controls defensible — hashing alone is not compliance without a documented lawful basis.

Step-by-Step Checklist

The checklist runs from discovery to enforcement: scan for raw PII, hash it before it enters the context, gate targeting on consent, invalidate caches on withdrawal, and expire logs on DPO-approved TTLs.

The five GDPR checklist steps Scan for raw PII, hash or exclude it, gate targeting on consent, invalidate cached context on consent withdrawal, and enforce retention TTLs on logs and audit events. 1 · Scan find raw PII 2 · Hash HMAC key 3 · Consent gate before eval 4 · Invalidate on withdrawal 5 · Retention TTL expiry
Steps 1–2 remove raw data; step 3 enforces lawful basis; steps 4–5 handle withdrawal and minimization over time.

Step 1 — Scan evaluation context for raw PII

Before changing any architecture, prove where PII currently lives. Run a pattern scan against evaluation logs and SDK context payloads:

# Scan flag-evaluation logs for raw PII field names
grep -rE '"(email|phone|ip_addr|ip_address|device_id|ssn|dob)"' \
  /var/log/flag-evaluation/ \
  | jq -r '.flag_key + " → " + keys[]'

Any hit is a compliance gap. Record each affected flag key, the field name, and the environment. This inventory drives the remediation in the next steps.

Cross-link: if your evaluation context flows through a distributed cache, the same PII appears in cached entries — see distributed caching for flag evaluations for cache invalidation on consent change.

Step 2 — Hash or exclude PII before SDK initialization

Apply a one-way HMAC hash to all personal attributes before they enter the evaluation context. Use a stable secret so the same input always produces the same output (needed for consistent bucketing), but rotate the secret annually so old hashes cannot be reversed:

import { createHmac } from 'crypto';

// compliance.context.js — sanitize before passing to OpenFeature
const HMAC_SECRET = process.env.PII_HMAC_SECRET; // from secrets manager

export const sanitizeEvalContext = (rawContext) => ({
  // stable pseudonymous identifier — consistent bucketing without storing email
  targetingKey: createHmac('sha256', HMAC_SECRET)
    .update(rawContext.email)
    .digest('hex'),
  // non-personal attributes pass through unchanged
  plan:     rawContext.plan,
  region:   rawContext.region,
  // consent state is passed as a boolean, never the CMP record
  consent:  rawContext.consentGiven,
  _meta: { sanitized: true, sanitizedAt: Date.now() }
});

Any flag that uses personal-data attributes for targeting must check consent before evaluating. Treat consent as a prerequisite flag — if compliance.consent.tracking-active is off for a user, fall back to the anonymous default variant:

import { OpenFeature } from '@openfeature/server-sdk';

const client = OpenFeature.getClient();

async function evaluateWithConsentGate(
  flagKey: string,
  ctx: EvaluationContext,
  defaultVariant: string
): Promise<string> {
  // Check consent prerequisite first
  const consentActive = await client.getBooleanValue(
    'compliance.consent.tracking-active', false, ctx
  );
  if (!consentActive) {
    // No lawful basis — return default without targeting
    return defaultVariant;
  }
  return client.getStringValue(flagKey, defaultVariant, ctx);
}

Validate in CI that every flag touching personal attributes declares a consent prerequisite:

# .github/workflows/flag-gdpr-validate.yml
- name: validate-gdpr-prerequisites
  run: |
    npx ajv-cli validate \
      --spec draft7 \
      -s ./schemas/gdpr-flag.json \
      -d './flags/*.json'

The gdpr-flag.json schema requires a prerequisites array containing "compliance.consent.tracking-active" on any flag with data-classification: personal.

A consent withdrawal must propagate to every caching layer within the latency budget specified in your GDPR DPIA (typically seconds, not minutes). Wire your CMP’s consent-revoked event to a cache invalidation call:

# consent_webhook.py — triggered by CMP on consent withdrawal
import hashlib, hmac, os, httpx

HMAC_SECRET = os.environ["PII_HMAC_SECRET"].encode()

def on_consent_revoked(user_email: str) -> None:
    user_hash = hmac.new(HMAC_SECRET, user_email.encode(), hashlib.sha256).hexdigest()
    # Purge the user's cached evaluation context from the flag cache
    httpx.post(
        "https://flag-cache.internal/v1/invalidate",
        json={"targeting_key": user_hash},
        headers={"Authorization": f"Bearer {os.environ['CACHE_API_TOKEN']}"},
        timeout=5.0
    )

Step 5 — Enforce retention TTLs on evaluation logs and audit events

Masking PII in the evaluation context upstream means logs carry hashed identifiers, not raw personal data — but they are still subject to data minimization. Evaluation logs (which variant, what context hash, what timestamp) are typically retained for 30–90 days for debugging. Flag mutation audit events (who changed what rule) are retained longer (12–24 months for SOC2) but must expire too.

# retention-policy.yaml — reviewed and approved by DPO
evaluation_logs:
  ttl_days: 30
  pii_fields: none   # hashed at source
  storage: hot-only  # no cold-tier archive needed

flag_mutation_audit:
  ttl_days: 730      # 24 months — SOC2 requirement
  pii_fields: actor_email   # business data; DPO sign-off in record
  storage: hot → glacier after 90 days

Verification

After applying all steps, run the scan from Step 1 again. Expect zero hits:

grep -rE '"(email|phone|ip_addr|ip_address|device_id|ssn|dob)"' \
  /var/log/flag-evaluation/ \
  && echo "FAIL: raw PII found" || echo "PASS: no raw PII in evaluation logs"

Then simulate a consent withdrawal and confirm the cache is invalidated within your DPIA window:

# Simulate revocation; confirm cached entry is gone within 5 s
curl -X POST https://flag-cache.internal/v1/invalidate \
  -H "Authorization: Bearer ${CACHE_API_TOKEN}" \
  -d '{"targeting_key":"<hmac-hash-of-test-user>"}'
sleep 5
curl -sf "https://flag-cache.internal/v1/entry?key=<hmac-hash>" \
  | jq '.found'   # expect false
Two GDPR verification checks Re-running the PII scan must return zero raw hits, and a simulated consent withdrawal must remove the cached entry within the DPIA latency window. PII re-scan 0 raw hits no email / IP / device in logs consent withdrawal cache gone < 5 s within the DPIA window
Compliance is proven, not assumed: the scan must come back empty and a revocation must purge the cached context inside the DPIA latency budget.

Gotchas & Edge Cases

Edge caches can lag a consent withdrawal A consent withdrawal invalidates the origin cache quickly, but an edge or CDN cache may keep serving the targeted variant for minutes; bypass the edge or set no-store for consent-sensitive flags. withdrawal CMP event origin cache purged < 5 s edge / CDN may lag minutes bypass the edge or set Cache-Control: no-store for consent-sensitive flags
The origin purges fast, but an edge cache can keep serving a targeted variant — so consent-sensitive flags must bypass the edge or mark responses no-store.

Troubleshooting & FAQ

Why does hashing the email still count as personal data?

A pseudonym is still personal data under GDPR if re-identification is possible with the key — and since you hold the HMAC secret, re-identification is trivially possible. That is fine: pseudonymization is a safeguard that reduces risk and satisfies Article 25 data-by-default requirements. It does not remove the data from GDPR scope entirely. The point is to prevent the data from appearing in logs or exports without protection, not to claim it stops being personal data.

Yes, with a short TTL (5–10 seconds). Cache the result of compliance.consent.tracking-active per user in the application layer, not in the flag provider. On consent withdrawal, invalidate the consent cache entry (same webhook as Step 4) before or simultaneously with the evaluation-context invalidation. Do not cache consent state longer than your DPIA latency budget for revocation propagation.

Anonymous users have no consent record, so compliance.consent.tracking-active is false by default. The consent gate in Step 3 returns the default variant — fully anonymous traffic gets the control experience. If you want to A/B test anonymous users without personal targeting, use a random or session-based targetingKey that is not derived from any personal attribute.