← Back to home

GDPR Compliance & Privacy

How doable-server and doable-tracker handle personal data, and what consent gets you.

This document is intentionally plain-English. It describes what we collect, how long we keep it, and which deployment modes meet GDPR "affirmative consent" requirements out of the box.


TL;DR

doable offers two distinct ways to use the analytics stack:

Mode What it is GDPR coverage
Tracker-only You host your site anywhere (Vercel, Shopify, own server). You only embed dt.min.js and send events to https://doable.services/api/v1/track. Full (with cookie consent enabled)
Proxy-routed Your site runs behind the doable-server gateway (Apache → Flask proxy → backend). Full (with cookie consent + proxy data minimization)

The tracker-only mode is designed to be GDPR-compliant by default: no personal data is retained without explicit consent. One admin checkbox enables the consent banner and the job is done.

The proxy-routed mode requires the additional proxy-level controls (IP truncation, DNT as fallback, consent-aware aggregation) because the proxy necessarily sees every request before the tracker's consent logic runs.

Quick reference: decision precedence

Signal from visitor Result
Clicked Accept on banner Full tracking — Accept is explicit consent, overrides DNT
Clicked Decline on banner No tracking, minimal access log only (proxy mode)
No banner interaction + DNT: 1 header No tracking (DNT fallback when no explicit decision)
No banner interaction + no DNT Depends on require_consent: banner shown if required; tracked otherwise
Revoked previously-given consent Reverts to "no decision" state; banner re-shown on next init

Case 1: Tracker-only sites

What this looks like

Your site (e.g., mysite.com) is hosted wherever you like. You add this to your HTML:

<script src="https://doable.services/static/dt.min.js"></script>
<script>
  DoableTracker.init({
    trackingId: 'dt_xxx',
    apiKey: 'your_api_key'
  });
</script>

Your site's server never talks to doable-server. The only communication is: 1. The visitor's browser fetches dt.min.js from doable.services 2. The tracker fetches /api/v1/track/config/<tracking_id> to check consent settings (cached 1h) 3. The tracker posts events to /api/v1/track (only after consent granted)

What doable-server collects for tracker-only sites

Endpoint What's logged Where Retention
GET /static/dt.min.js IP, UA, timestamp Apache access log only 14 days (logrotate)
GET /api/v1/track/config/<id> IP, UA, timestamp Apache access log only 14 days (logrotate)
POST /api/v1/track (consent denied) Nothing (tracker never sends)
POST /api/v1/track (consent granted) Event JSON, hashed session ID, hashed IP ExternalAnalyticsEvent 7 days (raw), indefinite (aggregates)
POST /api/v1/track/consent (on Accept only) Session ID hash, consent text hash, privacy URL, UA hash, timestamp ConsentLog 7 years (Art 7(1) audit)

Critically: the /api/v1/track endpoint bypasses proxy.py → log_access() entirely. No entries are written to AccessLog. No Graylog entries. The only persistent storage is ExternalAnalyticsEvent (raw events) and ExternalAnalyticsDaily (aggregated counters). Both handle IPs as SHA256 hashes, never raw.

Before v2.6.23: tracker fires on every pageview regardless of visitor preference.

As of v2.6.23: admin toggles "Require cookie consent" on the tracking project. Tracker then: 1. Shows a neutral bottom banner with Accept and Decline buttons of visually equal prominence (GDPR Art 7(2) requires this) 2. Stores decision in the dt_consent cookie (SameSite=Lax; Secure; Path=/; Max-Age=31536000). Cookie is used rather than localStorage so the proxy layer can read the same decision. 3. Sends events only on granted; stays silent on denied 4. Creates the session ID only after Accept — before consent, nothing is written to the visitor's device (fixes ePrivacy Art 5(3) compliance) 5. On Accept, posts a lightweight consent_granted event to /api/v1/track/consent for audit trail (Art 7(1) accountability) 6. Shows an always-visible floating revoke icon (bottom-left, 32×32px) so withdrawing consent is as easy as giving it (Art 7(3))

No visitor is tracked in doable's systems without an affirmative Accept click. Decision persists across sessions and browser restarts. Revocable via the floating icon, doable('revoke'), or clearing the cookie.

Why this is GDPR-aligned out of the box

What's NOT covered


Case 2: Proxy-routed sites

What this looks like

Your site (e.g., tomasandre.se) routes through doable-server:

Internet → Apache (443) → Flask gateway (5000) → Backend on 192.168.x.x

The proxy sees every request and can inject metadata, enforce auth, rate-limit, and — historically — log everything about every visitor.

What the proxy logs

Layer What's captured Where Default retention
Apache IP, UA, Referer, path /var/log/apache2/doable-server-access.log System log rotation
Flask AccessLog IP (full), UA, path, query string, status, response time, user_id if logged in access_logs table Indefinite (pre-v2.7.0)
Flask RouteAnalyticsDaily Daily aggregated browser/OS/device/UTM/referrer counters per route route_analytics_daily table Indefinite
Flask RoutePerformanceDaily Response time percentiles, status code counts route_performance_daily Indefinite
Graylog (optional) Extended headers: Accept-Language, Sec-CH-UA, DNT, referer, timing Graylog GELF UDP Per Graylog config

Important: all of this happens at the gateway — before the tracker ever loads in the visitor's browser. Cookie consent on the tracker alone doesn't prevent the proxy from recording the request.

What v2.7.0 changes (proxy data minimization)

As of v2.7.0, with defaults applied:

  1. IP truncation (pseudonymization)AccessLog.client_ip stores 192.168.44.0 instead of 192.168.44.123. Raw IP is kept only in-memory during the request for ban-check and rate-limiting. Configurable via PROXY_IP_TRUNCATE (default on). Note: /24 truncation is pseudonymization under GDPR Art 4(5), not full anonymization — a residence or small office may still be identifiable.
  2. Consent-aware aggregation — visitor declines the cookie consent banner → dt_consent=denied cookie set → proxy reads it and skips update_analytics(). Only minimal access log entry (truncated IP, method, path, status) remains.
  3. DNT as fallbackDNT: 1 header skips analytics aggregation only when no explicit banner decision has been made. Explicit Accept on the banner overrides DNT (the banner is specific and informed; DNT is a general preference).
  4. Retention policyAccessLog entries older than 90 days (configurable) are deleted by a scheduled cleanup script. Aggregates kept 2 years (already depersonalized). Consent audit logs kept 7 years (GDPR statute of limitations).
  5. Apache log rotation mandate — deployment must include logrotate config with short retention (default 14d) to minimize residual IP exposure from pre-consent requests (dt.min.js and /api/v1/track/config/<id> fetches).

Together, cookie consent + proxy data minimization mean: - Consented visitor → full analytics, as today - Declined visitor → minimal truncated-IP access log only, no browser fingerprinting or UTM tracking stored - DNT visitor with no banner decision → same as declined - DNT visitor who clicked Accept → full analytics (explicit consent wins)

Known limitation: first-request blind spot

On proxy-routed sites with require_consent_proxy=True, the first request from a fresh browser cannot be aggregated — the banner hasn't rendered yet, so there's no cookie to read. Subsequent requests after Accept are aggregated normally. This means cumulative analytics slightly undercount unique visitors, but it's the correct behavior under GDPR (no consent = no tracking).

Why this is GDPR-aligned


Data collected per endpoint (full table)

Endpoint Mode Data collected Retention Minimized by
GET /static/dt.min.js Both Apache access log (IP, UA) System rotation Proxy IP truncation (pseudonymization) if routed via proxy
GET /api/v1/track/config/<id> Both Apache access log (IP, UA) System rotation Cached 1h — 1 request per unique visitor per hour
POST /api/v1/track Tracker-only event_data (full JSON), client_ip_hash, session_id, url, path 7 days (raw), indefinite (aggregates) anonymize_ip=true zeroes last octet before hashing; cookie consent gates entirely
POST /api/v1/track/beacon Tracker-only Same as above Same Same
Proxied request (any) Proxy-routed AccessLog + analytics aggregation 90d / 2y (v2.7.0+) IP anonymized, consent-aware, DNT-aware

Comparison with other analytics

Tracker Minified size Network requests per pageview Consent UX Data minimization Self-hosted analytics backend
Google Analytics 4 ~50 KB 2-4 Requires CMP integration (Cookiebot, OneTrust, etc.) Google keeps raw data No
Matomo (self-hosted) ~25 KB 1-2 Has consent mode, integration required Full control Yes
Plausible ~1 KB 1 Cookie-less by design (uses fingerprint hash) No raw IPs, minimal data Yes
doable-tracker ~5 KB 1 (cached) Built-in, one admin checkbox Hashed IPs, 7d raw retention Yes (doable-server)

doable-tracker sits between Plausible's minimalism and Matomo's full-feature approach, with consent being a toggle rather than a separate integration.


Retention policy (defaults)

Data Default retention Configurable via
AccessLog (proxy requests) 90 days (v2.7.0+) ACCESS_LOG_RETENTION_DAYS
RouteAnalyticsDaily (aggregates) 2 years ANALYTICS_RETENTION_DAYS
RoutePerformanceDaily 2 years ANALYTICS_RETENTION_DAYS
ConsentLog (Art 7(1) audit trail) 7 years CONSENT_LOG_RETENTION_DAYS
ExternalAnalyticsEvent (raw tracker events) 7 days Existing cleanup
ExternalAnalyticsDaily (tracker aggregates) Indefinite (depersonalized) Not configurable
Apache access logs 14 days (logrotate) /etc/logrotate.d/apache2-doable-server
Graylog Per Graylog retention config Graylog admin (configure separately)

Aggregates are JSON counters like {"Chrome": 1043, "Firefox": 198} — they contain no per-user data and are safe to retain indefinitely for trend analysis.


For site operators: controller/processor relationship

Under GDPR Art 4(7-8), when you (the site operator) use doable-tracker to collect analytics about your visitors, you are the data controller and doable.services is the data processor acting on your instructions.

Data Processing Agreement (DPA) required

GDPR Art 28 requires a written DPA between controller and processor. The DPA must specify:

A template DPA is available at docs/dpa-template.md (forthcoming — contact your account). Customize with: - Your organization's legal entity details - The tracking project IDs covered - Retention periods matching your privacy policy

Sign it, keep a copy, and reference the signed DPA in your privacy policy.

Data transfer (international)

doable.services infrastructure is hosted in Sweden (EU). Visitor data is: - Collected in the EU - Processed in the EU - Stored in the EU

This means no international transfer under GDPR Art 44-49 for EU/EEA-based site operators and visitors. No Standard Contractual Clauses (SCCs) required.

If doable.services ever moves infrastructure outside the EU, site operators will be notified in advance and SCCs will be added to the DPA. Current status is always documented in config.py comments and at the top of docs/gdpr-compliance.md.

For site operators outside the EU: your local data protection law may still require equivalent agreements. Verify with a DPO.

Visitor rights

Per GDPR Art 15-22, visitors can request:

Right How to handle
Access Aggregates contain no identifiable data. Raw events are retained 7 days and can be looked up by session_id or client_ip_hash if the visitor provides these.
Rectification Not applicable — we store no user-editable profile data.
Erasure Send session_id or IP hash → delete matching rows from ExternalAnalyticsEvent. Aggregates don't contain per-user data.
Restriction Visitor sets dt_consent=denied or uses doable('revoke') — future tracking stops.
Portability Not applicable — no portable dataset per user.
Object Same as restriction.

Site operators using doable-tracker should include this in their privacy policy.


Configuring for GDPR compliance (checklist)

Tracker-only sites

Proxy-routed sites (in addition to tracker-only)


AI Insights privacy note

Doable AI Insights analyses each resource's analytics summary once a day using a local Ollama instance. Specifically:

Nothing about the AI Insights pipeline expands the personal-data surface established by cookie consent and proxy data minimization — it is a downstream consumer of the same already-minimized aggregate data.

Google Search Console privacy note

The GSC integration uses a shared service account authenticated to properties that users explicitly add the service-account email to as a Restricted user in Google Search Console. Specifically:


Version history of privacy features

Version Change
v2.2.1 External tracking API introduced. IP hashing in ExternalAnalyticsEvent from start.
v2.6.21 Human-only audience breakdowns (bot filtering reduces apparent data collection).
v2.6.23 Cookie consent for tracker: dt_consent cookie, floating revoke icon, ConsentLog audit trail, lazy session ID, explicit Accept overrides DNT. Full GDPR coverage for tracker-only mode.
v2.7.0 Proxy IP truncation (pseudonymization), consent-aware aggregation, DNT as fallback at proxy, retention cleanup (90d access logs, 7y consent logs), Apache logrotate mandate. Full GDPR coverage for proxy-routed mode.
v2.8.0 Doable AI Insights pipeline runs on local Ollama; no data leaves the server. Email alerts contain finding text + dashboard link only. Per-user opt-out.
v2.9.0 Google Search Console integration. Aggregate-only data, service-account scope limited to user-granted properties, cascade-purge after disconnect.
v2.9.1 AI Insights and GSC privacy notes consolidated into this document. No behavioral change.

Disclaimer

This document describes the technical capabilities of doable-server and doable-tracker. It does not constitute legal advice. GDPR compliance depends on your specific deployment, jurisdiction, the nature of data you collect beyond what doable tracks, and your documented lawful basis. Consult a qualified data protection officer or lawyer for your use case.