Security

ClickStream is built on the principle that operators should be able to verify our security posture without taking our word for it. Every sensitive pathway — raw-PII reveal, API key rotation, admin actions — is logged, rate-limited, and scoped to a specific operator identity.

This page covers the production security posture. For day-to-day privacy controls (consent, banners, data residency), see Privacy & compliance.

Encryption at rest

Every field containing raw PII is AES-256-GCM encrypted before it touches persistent storage, under a unique symmetric key per customer site. Keys are managed via Cloudflare's Secrets Store binding; the collector never sees plaintext keys — it requests encrypt / decrypt operations via a narrow API.

Encrypted surfaces:

The encrypted blobs live in Dashboard D1 (visitor_encrypted_fields, form_submissions). The SHA-256 hashes live in Analytics Engine (blob11 / blob12 / blob20 per the event schema).

Per-tenant HMAC isolation

Hashed identity signals in Analytics Engine are HMAC'd with a per-tenant key before storage. That means:

Raw-value reveal — the /decrypt gate

Operators with decrypt:read permission can reveal raw encrypted values on a per-visitor basis via the dashboard. Every reveal passes four independent checks:

  1. Authentication — valid dashboard session.
  2. Permission — operator's role includes decrypt:read.
  3. Password re-authentication — operator has re-entered their password within the last 5 minutes (/api/auth/decrypt-unlock). Session auth alone isn't enough; re-auth is required for every sensitive action.
  4. Rate limit — 10 reveals per operator per 5-minute window.
  5. Audit — an audit_log row is written with operator id, IP, visitor id, field type, timestamp, and the required free-text reason ("suspected fraud", "user support ticket #1234", …).

The audit log is append-only. Admins see the full reveal history on the Security → Reveal Audit tab, filterable by operator and date. Audit rows are retained 7 years (SOC2 window) and exportable as CSV.

API key lifecycle

Keys are opaque bearer credentials. A compromised key grants whatever scopes it carries until revoked — same trust model as Stripe restricted keys. Treat them accordingly: CI secret managers, per-environment keys, cs_test_ prefix for staging.

Admin surface — X-Admin-Key

A handful of administrative endpoints (custom hostname provisioning, KV bootstrap, force-refresh cache) require the ADMIN_API_KEY secret via X-Admin-Key header instead of the standard API key. The admin secret is set via wrangler secret put ADMIN_API_KEY and rotated quarterly. It's not issued to customers — the dashboard calls these endpoints on your behalf when you, say, provision a new tracking domain.

Rate limiting

The collector enforces per-API-key rate limits with a sliding-window counter backed by a Cloudflare Durable Object (SOC2-friendly "authoritative source"). Default caps per tier:

TierEvents / sec
Free100
Builder1,000
Scale5,000
Network25,000
Custom100,000

Burst allowance is 2× the sustained rate for short spikes. See Rate limits for the response headers + overage behavior.

TLS / transport

Content Security Policy

The collector returns CSP headers on every response (content-security-policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'). The SDK loader + bundle are served from your first-party domain, so the customer's own CSP can allow-list t.yourdomain.com without needing a third-party domain exception. Per-site CSP overrides are configurable on the Site → Security tab.

Audit log — what's captured

Every action of consequence on the dashboard writes to audit_log:

Retention: 7 years, append-only. Exportable as CSV from Security → Audit Log.

Incident response

Roadmap

Data residency

Covered on the Privacy & compliance page. Cloudflare's data-at-rest regions are enforced via tenant-specific wrangler.toml on Custom tier; all other tiers inherit Cloudflare's default global distribution.

See also