8 MIN READ · Pedro Thomaz

GDPR-compliant analytics without cookies: what we built and why there's no banner

GDPR-compliant analytics without cookies is possible when you collect no PII and set no client-side identifier — which is exactly why our sites carry no cookie banner.

GDPR-compliant analytics without cookies: what we built and why there's no banner

GDPR-compliant analytics without cookies is possible only when you collect no personal data and store no identifier on the visitor's device. Get those two things right and the legal basis for a cookie banner disappears — there is nothing to consent to. This is the route we take on every site we ship, including this one. Below is exactly what that means, what you can and can't measure, and the first-party tracker we built to stay on the right side of the line.

What "GDPR-compliant analytics without cookies" actually means

The phrase gets thrown around loosely, so let's define it. Analytics is truly cookieless and consent-free when it satisfies two independent legal tests at once:

Pass both and you've earned the thing every marketing team secretly wants: no banner, no consent flow, and analytics that still works for 100% of visitors instead of the ~60% who click "accept". Most "cookieless" tools on the market pass the first test and quietly fail the second, because they still hash an IP and a user agent into a daily visitor ID. That is a device-derived identifier and, under Breyer reasoning, very likely personal data.

The short version

What you can measure without cookies (and what you can't)

Honesty matters more than the pitch here, because the tradeoff is real. Cookieless, identifier-free analytics is aggregate-only. You are counting events, not following people.

What you can answer cleanly:

What you cannot answer, and shouldn't pretend to:

For a studio site, a marketing site, a portfolio, or most content businesses, the aggregate view is enough to make decisions. If your business model genuinely needs per-user attribution — a high-velocity e-commerce funnel, say — then you need consent, and you should ask for it honestly with a real banner rather than dress up tracking as "privacy-first". We told Delicious Diamonds exactly that for their members-only commerce flows: the public marketing site runs cookieless, the authenticated purchase journey uses first-party session data the customer has already agreed to by logging in.

How we built it: one beacon, no identity

Our stack is deliberately boring — PHP 8.3 on shared hosting, Cloudflare in front, no build step, server-rendered. The tracker fits that philosophy. There is no third-party script, no SDK, no npm dependency. It's a few lines of vanilla JavaScript and one PHP endpoint.

The client fires a single POST roughly three seconds after load (long enough to skip most bounces and bots), then forgets it ever happened:

// no cookies, no localStorage, no identity
navigator.sendBeacon('/api/hit', JSON.stringify({
  path: location.pathname,
  referrer_origin: document.referrer
    ? new URL(document.referrer).origin
    : null,
  lang: navigator.language.slice(0, 2),
  screen_w: window.innerWidth,
  ts: Date.now()
}));

Read that payload carefully, because the privacy guarantee lives in what's absent. There is no user agent. No IP — and critically, the server is configured never to log or persist the connecting IP for these requests. No UUID, no hash of anything, no session token. referrer_origin deliberately strips the path and query string off the referrer, so we learn you came from LinkedIn but not which post or which UTM trail. The language is truncated to two characters. None of these fields, alone or combined, singles out a person.

On the server, the endpoint appends one line of JSON to a monthly file:

// /api/hit -- append-only, no IP, no identity
$line = json_encode([
    'path'     => $clean['path'],
    'ref'      => $clean['referrer_origin'],
    'lang'     => $clean['lang'],
    'w'        => (int) $clean['screen_w'],
    't'        => time(),
]) . "\n";

file_put_contents(
    __DIR__ . '/../storage/hits/' . date('Y-m') . '.jsonl',
    $line,
    FILE_APPEND | LOCK_EX
);

The storage/hits/ directory is locked down at the web-server level with an .htaccess carrying Require all denied, so the raw logs are never reachable over HTTP. A small PHP dashboard behind HTTP basic auth reads those JSON-lines files, aggregates by day, and renders six numbers and a bar chart. The whole thing runs on-server; no data leaves our infrastructure, and no third party — no Google, no analytics vendor — ever touches the visitor.

Why that means no cookie banner

Walk it back through the two tests.

Article 5(3): we store nothing on the device and read nothing back. sendBeacon is a fire-and-forget HTTP request; it sets no cookie and touches no storage. There is no "access to information stored in the terminal equipment", so the consent requirement of the cookie law is not triggered. Nothing to ask about.

GDPR: we process no personal data. No IP is stored, no identifier is derived, and none of the retained fields — a path, a referrer origin, a two-letter language, a viewport width, a timestamp — identifies a natural person, individually or in combination. With no personal data in scope, there is no Article 6 lawful basis to establish and no consent to collect.

Two tests passed, zero banners required. The legal conclusion is not a clever loophole; it's the direct consequence of having genuinely nothing to declare. The banner exists to obtain consent for storage and for processing personal data. Do neither and the banner has no job to do.

FAQ

Is Google Analytics ever GDPR-compliant without a banner?

No. GA4 sets cookies and processes identifiers and IP-derived data, which engages both the ePrivacy and GDPR tests. It requires consent in the EU, and several DPAs (Austria, France, Italy) have ruled standard GA configurations unlawful over US data transfers. If you run GA, you need a banner and a consent flow.

What about "cookieless" tools like Plausible or Fathom?

They're a big improvement and avoid cookies, but most generate a daily visitor hash from IP plus user agent to estimate uniques. That hash is a device-derived identifier and, following Breyer, is arguably personal data. They're privacy-respecting and often defensible without a banner, but it is a finer legal line than "we store and derive nothing", which is the line we chose to stand on.

Don't you lose important data?

You lose per-user data. You keep every aggregate number that actually informs a content or design decision, and you keep it for all visitors instead of the minority who accept a banner — which often makes the aggregate picture more accurate, not less.

Does Cloudflare break the no-PII promise?

Cloudflare sits in front as a CDN and sees IPs at the network layer, as any host does. The distinction that matters legally is that we don't log, store, or process those IPs in our analytics; the beacon payload contains no IP and the endpoint never records the connecting address. Network-level transit is not the same as collecting personal data into an analytics dataset.

If you want this

The pattern is portable. Any server-rendered stack can ship it: a single beacon, a strip-everything payload, an append-only first-party store, no third party, no identifier. The hard part isn't the code — it's the discipline to not collect the data that makes a banner mandatory. We build sites this way by default because it's faster, lighter, and it means the first thing a visitor sees is the work, not a consent dialog. If you want analytics you can defend without a privacy team and a legal review, this is the shape of it.