Skip to content

Edge Data Export Policy (Draft)

Edge Data Export Policy

Status: Draft for owner and legal review. This is an engineering policy, not legal advice. Retention periods, cohort thresholds and any cross-border transfer basis need sign-off before the first export.

This policy defines what data may leave Bloqr’s Cloudflare boundary (Workers, KV, D1, R2, Neon) and flow to any external system: a data lakehouse such as Databricks, a warehouse, an analytics SaaS, a partner, or a data-sharing product. It applies to every current and future exporter. Companion design: Filter List Intelligence.

Bloqr is a privacy product. The default is that nothing leaves, and each field that does must be named, classified and justified.

Principles

  1. Default deny. Only fields on an explicit export manifest may leave. A new field is not exported until someone adds it and a reviewer approves the classification.
  2. Minimize, then aggregate. Prefer public or operational data. If user-linked data is ever needed, export aggregates, never rows.
  3. Purpose limitation. Each export names the question it answers. Data collected for operations (abuse control, billing, debugging) is not reused for product analytics without review.
  4. No re-identification. Nothing exported, alone or joined with other exported data, may identify or single out a person, device or private configuration.
  5. Fail closed. If sanitization, validation or the kill switch fails, the export stops. It never falls back to sending raw data.

Data classes

ClassDefinitionExamples in this codebaseMay leave the edge?
0 — PublicDerived solely from public sources; identical for every user.Upstream list content, rule hashes, canonical_text of public rules, domains in public lists, list metadata, public threat-intel indicators.Yes, subject to the licence gate (E3).
1 — OperationalSystem behavior with no link to a person or private config.Source-health status and latency, queue depth, rule counts, compile duration buckets, HTTP status codes, error codes.Yes, via the manifest.
2 — PseudonymousCould be linked to a user, device or private config, directly or by joining.requestId, any client IP hash, API key id, user id, session id, sourceId of a user-defined source, per-user config names, timestamps at fine granularity combined with the above.Never as rows. Aggregates only, subject to E6.
3 — Personal or secretIdentifies a person or grants access.Raw IP, email, auth cookies and tokens, API keys, passwords, user-supplied source URLs (may embed credentials), free-text error messages and stack traces (may contain any of the above), private list content.Never.

When in doubt, classify upward.

Classification of the existing analytics tables

TableFieldClassExport decision
compilation_eventseventId2Exclude.
requestId2Exclude.
sourceId1 if a registry source, 2 if user-definedExport only registry sources (E3).
timestamp1Truncate to the hour.
eventType, status, format1Export.
durationMs1Export bucketed (E6).
ruleCount1Export bucketed.
errorMessage3Exclude. Replace with an error_code enum (E4).
source_healthsourceUrl1 if canonical registry URL, 3 otherwiseExport only the canonical registry URL (E5).
healthStatus, latencyMs, statusCode1Export.
errorMessage3Exclude; use error_code.
filter_rule_changesruleHash, changeType, sourceId0 (registry source)Export, subject to E3.
beforeRule, afterRule0 for public sources, 3 for privateExport only for public and redistributable sources.
threat_intel_hitsindicatorValue0Export if from a redistributable feed.
matchedRuleId, confidenceScore0Export.

Rules

E1 — Manifest allowlist. Each exported dataset has a checked-in export manifest listing every field, its class, and its transformation. The sanitizer drops any field not in the manifest. Schema changes to the source table do not change the export.

E2 — Class 3 never, Class 2 only aggregated. No Class 3 field is exported under any circumstance. Class 2 fields are exported only as aggregates that satisfy E6.

E3 — Public registry sources only. Only sources marked public: true in the source registry are exported. Anything user-defined, private, or fetched with credentials is excluded. List content, including rule text, leaves Bloqr only when the source is also redistributable: true; licences differ across popular lists (some share-alike, some non-commercial), and internal analysis rights are not redistribution rights.

E4 — No free text. Free-text fields (error messages, stack traces, user input) are never exported. Map them to a closed enum (TIMEOUT, HTTP_5XX, PARSE_FAIL, …) at the sanitizer.

E5 — URL canonicalization. Exported URLs have userinfo, query string and fragment removed and the host checked against the registry. A URL that cannot be matched to a registry entry is dropped, not truncated.

E6 — Aggregation thresholds.

  • Release a cell (a group-by combination) only if it contains at least k = 20 distinct underlying subjects. Suppress or merge smaller cells. k is a proposal; the owner sets it.
  • Use time buckets of at least one hour for Class 1 and one day for anything derived from Class 2.
  • Bucket continuous values (duration, rule count) rather than exporting exact values when the exact value adds no analytical benefit.
  • Check differencing: releasing overlapping aggregates must not allow a small cell to be recovered by subtraction. If in doubt, coarsen.

E7 — Pseudonymization. If a Class 2 identifier must be used inside the sanitizer to compute an aggregate (for example, counting distinct clients), use HMAC-SHA-256(secret, identifier || bucket), truncated to 128 bits, with the secret held in Worker Secrets and rotated at least daily so tokens cannot be linked across days. The pseudonym itself is never exported; only the resulting count is.

Known gap. AnalyticsService.hashIp (src/services/AnalyticsService.ts:589-598) is a 32-bit, unsalted, non-cryptographic hash. It collides heavily and the IPv4 space is small enough to enumerate, so it is not an anonymization control, and docs/cloudflare/CLOUDFLARE_ANALYTICS.mdx overstates what it provides (“Client IPs are hashed before storage”, “No PII”). Do not use it for anything that leaves the edge. Replace it with the E7 construction, or stop storing client-IP-derived values, before any exporter that touches Class 2 data is enabled. Correct the documentation either way.

E8 — Dedicated identity and least privilege. Exporters run as their own Worker or Workflow with a credential scoped to read the export/ prefix only, and a write-scoped, short-lived credential for the destination (for example an OAuth service principal, not a personal token). Exporters hold no database credentials and cannot read raw Class 2/3 stores. Secrets live in Worker Secrets, never in wrangler.toml [vars], consistent with the repository’s Zero Trust rules.

E9 — Two-stage pipeline with a quarantine.

flowchart LR
  RAW[Raw stores: D1, KV, Neon, R2 logs] --> SAN[Sanitizer: Zod + manifest]
  SAN -->|valid| EXP[R2 export/ prefix]
  SAN -->|invalid or unclassified| Q[Quarantine + alert]
  EXP --> OUT[Exporter]
  OUT --> DEST[(External system)]
  KILL{Kill switch} -.blocks.-> OUT

The sanitizer validates every record against the manifest schema (Zod, as elsewhere in this codebase). Records that fail go to quarantine and raise an alert; they are never forwarded. Exporters can read only export/.

E10 — Retention and deletion.

  • Class 0 and 1 exports: retain up to 13 months unless the destination’s cost or policy requires less. Proposal.
  • Aggregates derived from Class 2: retain no longer than 90 days at their finest grain, then roll up. Proposal.
  • The destination must support deleting a dataset by export run, so a faulty run can be withdrawn.
  • Because no user-level rows are exported, individual deletion requests do not need to propagate downstream. Verify this holds for each new exporter.

E11 — Residency and transfer. Record the region of every destination. If any users are in jurisdictions with transfer restrictions, document the legal basis (for example, standard contractual clauses) and complete a data protection assessment before enabling the exporter. Requires legal review.

E12 — Audit trail. Each export run writes an audit record: manifest hash, row counts in, out and suppressed, destination, and the run’s identity. Alert on schema drift (a source table gained a field the manifest does not mention) and on any non-zero quarantine count.

E13 — Processors and disclosure. Before the first export to a third party: execute a data processing agreement, add the vendor to the subprocessor list, and update the public privacy policy and any privacy-related marketing claims so they match what actually happens.

E14 — Kill switch. Every exporter is behind a feature flag (the Worker already binds [[flagship]]) that defaults to off, and is surfaced in the admin panel with its description, as required by the repository’s admin governance rules.

Enforcement

Policy without checks decays. Proposed controls, in order of cost:

  1. Manifest drift test. A Deno test that fails if a source table’s schema gains a field absent from its export manifest. This mirrors the existing check:drift:admin pattern.
  2. Sanitizer unit tests. Golden cases per dataset: every Class 2/3 field is stripped, free text is mapped to enums, URLs are canonicalized, cells below k are suppressed.
  3. Differencing test. Property-style test that overlapping aggregate releases cannot recover a suppressed cell.
  4. PR checklist. Any change touching an exporter or manifest requires an explicit classification note.
  5. Periodic review. Quarterly sample of exported objects against the manifest.

What this policy does not cover

  • Data processed entirely inside Cloudflare (for example, Analytics Engine queried from the dashboard).
  • Customer-facing exports requested by that customer about their own data.
  • Cloudflare’s own handling of traffic and logs, which is governed by the Cloudflare agreement.

Open decisions for the owner

DecisionProposalWhy it matters
Value of k20Higher is safer but suppresses more cells on a small user base.
Retention periods13 months / 90 daysDrives destination cost and risk surface.
Whether any Class 2 aggregates are exported at allNo, until the Class 0/1 pipeline proves its valueRemoves the main privacy risk while the product question is open.
Replace or retire hashIpReplace with the E7 constructionNeeded before any user-level aggregate exists, and to fix current documentation.
Legal review of transfer basisBefore first third-party exportDepends on user geography.