Engineering

GDPR as Cron Jobs: Compliance You Can Read in the Source

Retention windows, erasure requests, and PII scrubbing are not paragraphs in our privacy policy. They are scheduled jobs with budgets, audit rows, and failure alarms.

July 2026 • 9 min read

Compliance Pages Are Promises. Schedulers Are Facts.

Most analytics vendors handle GDPR with a trust page: a reassuring paragraph about taking privacy seriously, a link to a DPA, and an email address for deletion requests. The problem is structural. A policy document can promise anything. The only thing that actually deletes data on a schedule is a scheduler.

This essay is about GDPR analytics engineering the way we practice it: retention as a cron job that runs every five minutes, erasure as an authenticated endpoint that propagates through five storage systems, and PII scrubbing as a function that runs at the edge before anything is stored. Every mechanism below is a specific job, route, or table in the ClickStream platform — including the genuinely hard parts, like data already written to immutable storage.

A privacy policy tells you what a vendor intends. A cron schedule tells you what actually happens at 3 a.m. when nobody is watching.

Retention Presets That Expand into Code

Every ClickStream site carries a compliance profile built from one of five presets: standard, gdpr_strict, hipaa, ccpa, or custom. A preset is not a label on a settings page — it is a function that expands into a concrete configuration object consumed by the collector, the SDK, and the purge jobs:

// packages/shared-types — expandPreset('gdpr_strict'), verbatim
{
  preset: 'gdpr_strict',
  consentMode: 'opt_in',
  scrubLevel: 'aggressive',
  allowThirdPartyIdentity: false,
  allowMarketingIdentity: false,
  allowDeviceFingerprint: false,
  allowThirdPartyEnrichment: false,
  hipaaMode: false,
  retentionDays: 180,
  autoPurge: true,
}
Preset Consent Mode Scrub Level Retention Auto-Purge
standard Opt-out Standard 90 days On
gdpr_strict Opt-in Aggressive 180 days On
hipaa Opt-in Aggressive 90 days On
ccpa Opt-out Standard 90 days On
custom Starts from standard defaults Configurable 30–3,650 days Configurable

Every preset ships with auto-purge on — there is no preset that retains visitor data forever by default. And the profile is replicated to the collector's edge configuration, so consent mode and scrub level are enforced server-side from the first request — not by JavaScript that a slow page load might skip. The same profile also carries identity toggles (third-party identity, marketing identity, device fingerprinting), which interact directly with how first-party cookie persistence works on a site, and an excludedPaths list: path prefixes like /checkout for which the collector drops form values, replay frames, and behavioral summaries entirely, enforced server-side.

Session replay follows the same philosophy with its own tiered windows — we wrote about that separately in the honest retention ladder.

The Retention Purge Cron Runs Every Five Minutes

A retention setting is only a promise until something enforces it. On our platform, the enforcement is a purge job triggered by the export worker's scheduled handler every five minutes. Each run:

The cadence itself shows what treating compliance as code actually means. The job originally rode an hourly cron. Each run has a hard budget of 200 visitors — pinned by a platform subrequest limit that cannot be raised — so hourly runs capped the entire platform at 4,800 purged visitors per day. Measured inflow of purge-eligible visitors averaged around 5,150 per day with peaks over 10,000: the queue was quietly falling behind, a compliance bug even though nothing was "broken." Since the budget could not grow, cadence was the only lever — the job moved to the five-minute tick, raising the ceiling to 57,600 per day.

And because a purge that silently falls behind is the failure mode that matters, each run also probes the backlog of the least-recently-purged site and logs a warning when it exceeds ten runs' worth of budget. The drain-versus-inflow signal is part of the job, not a dashboard someone has to remember to check.

Article 17 as an Endpoint, Not an Email Alias

The GDPR right to erasure is implemented as an authenticated dashboard endpoint. It requires a session, site ownership, and an explicit confirmation flag — the request fails without it:

POST /api/privacy/delete
{
  "siteId": "site_abc123",
  "identifierType": "email",        // email | phone | hem | visitor_id
  "identifierValue": "subject@example.com",
  "confirm": true                   // schema-enforced literal; omitting it rejects the request
}

One request propagates through every system that might hold the data subject's records:

Each request is recorded in a data_subject_requests table, so there is a durable record that the DSAR happened, when, and what it covered. This is the same machinery that keeps the promotion path from anonymous visitor to CRM contact clean — covered in the compliant path from visitor to contact.

The Honest Part: What Erasure Cannot Do Instantly

Two of our storage layers are append-only, and pretending otherwise would be marketing, not engineering. The erasure endpoint states its own limitations in its response:

In other words: live systems forget immediately, append-only systems forget within their bounded retention window, and the endpoint tells you which is which.

Erasure-Aware Exports Fail Closed

Raw Parquet exports (available on request on Scale and higher plans) are the place where a deleted visitor could most easily leak back out. So every export run starts by loading the erased-visitor set for the tenant:

-- export worker, verbatim: the exclusion set loaded before any export
SELECT DISTINCT et.visitor_id
FROM erasure_tombstones et
JOIN sites s ON s.id = et.site_id
WHERE s.client_id = ? AND et.expires_at > ?

That set is passed into the Parquet writer as an exclusion list, so erased visitors are filtered out of every future export window. And the failure mode is the right one: if the export worker cannot resolve the tenant's encryption key or its exclusion set, the export aborts rather than shipping data it cannot vouch for. Exports are also encrypted per tenant with the customer's own key — the subject of its own essay, Your Data, Your Key.

PII Is Scrubbed at the Edge, Before Storage

Data you never store is data you never have to purge. Before any event reaches analytics storage, the collector sanitizes free-text fields — URLs, referrers, element selectors — server-side at the edge. The intensity follows the site's scrubLevel:

Payment card numbers get special treatment: they are redacted unconditionally at both levels. A candidate is matched broadly (13–19 digits in any script, any common separator), then confirmed by digit count and a Luhn check after Unicode normalization — so real card numbers are caught even in fullwidth digits, while legitimate long numbers like invoice and tracking IDs pass through untouched. That backstop exists specifically for the case where an SDK-side blocklist misses or a stale cached bundle is still running.

The Identity Graph Cleans Up After Itself

Person-level resolution creates its own privacy obligations, so the identity graph service runs its own maintenance cron every six hours:

// identity-graph scheduled handler — cron: 0 */6 * * *
pruneExpiredEdges()             // expired edges + household clusters
garbageCollectMergedNodes(90)   // merged nodes older than 90 days
scrubExpiredPii(90)             // raw PII in audit records older than 90 days
processResolutionTasks(100)     // reprocess ambiguous graph states

The third step is the one that matters most for this essay: any raw PII held in the graph's audit records is scrubbed once those records are 90 days old. Audit trails are kept; the personal data inside them is not kept a day longer than the window requires. How the graph is structured — and why edges expire at all — is covered in Inside the Identity Graph.

Consent Receipts: The Audit Trail That Survives

Consent is recorded the same way everything else here is: as rows, not vibes. Every consent decision is written to a consent_receipts table tied to the visitor. Those receipts then follow the data subject through every right they exercise:

What This Essay Is Not Claiming

It is worth being explicit about boundaries. This essay makes engineering claims, not certification claims — we are describing jobs and endpoints, not waving a badge. The operational constants it quotes (per-run budgets, cadences, measured inflow) are current design decisions that will evolve, not service-level guarantees. And it does not claim erasure is instantaneous everywhere — the append-only layers forget on their bounded 90-day schedule, and we would rather tell you that than pretend.

The same posture runs through the rest of the platform: API keys are scoped and domain-gated so a leaked key is useless (covered here), and the whole pipeline is built so that you own your analytics data rather than renting access to it.

The Bottom Line

GDPR compliance in an analytics platform is not a document problem. It is a distributed-systems problem: retention windows that must be enforced against real inflow, erasure that must propagate through databases, edge caches, graphs, and export pipelines, and audit trails that must outlive the data they describe. The mechanisms above — a five-minute purge cron with a backlog alarm, an Article 17 endpoint that fans out to five systems, exports that fail closed, and PII scrubbing that runs before storage — are how we turned that problem into running code.

If a vendor's deletion story is an email alias, their retention story is a shrug. Ask to see the scheduler.

Retention You Configure. Erasure That Propagates.

Pick a compliance preset, set your retention window, and let the schedulers do what the policy pages of other vendors only promise.

Start free