Coruve

Data & privacy

Privacy & data handling

What Coruve collects, what it structurally cannot collect, where it is stored, how long it is kept, and what a language model is ever shown. Written for the engineer answering a security questionnaire.

The short version

Four sentences you can paste into a security questionnaire.

Coruve stores no IP addresses, sets no cookies, and builds no persistent or cross-site identifier. The visitor identifier it does use is derived in the browser and changes every day by construction, so there is nothing to link one day's visits to the next, let alone one site's to another's. Do Not Track and Global Privacy Control turn the tracker off entirely. Nothing here is a setting you have to find — it is how the tracker is built.

For the wording that governs the contract — controller and processor roles, your rights, sub-processors, international transfers — the privacy notice, the DPA and the cookie notice linked at the bottom of this page are the authoritative documents.

IP addresses

Looked at once, in memory, and then gone.

The IP is discarded, not hashed

Every request that reaches the ingest service carries an IP address, as every HTTP request does. Coruve uses it for exactly two things, in the same function that receives it, and then discards it: it resolves the address to a coarse country, region and city, and it mixes the address into the one-way daily visitor identifier described below. The address itself is never written anywhere. There is no IP field in the event schema at all, so there is no column for one to be written into by accident later, and nothing stored can be turned back into an address — the identifier is a SHA-256 digest that also contains a secret only Coruve's servers hold, and it is discarded and rebuilt from scratch every day.

What this buys you

Geography in your reports without an identifier in your data store. The Locations report is built from those three labels and nothing else.

The visitor identifier

Computed in the browser, rotates daily, and cannot cross a site boundary.

How it is built

The tracker takes your project ID, the browser's user-agent string, its language, its timezone offset and today's date, joins them, and hashes the result with SHA-256 in the browser. Two facts fall out of that recipe. Because today's date is in the input, the identifier changes at midnight every day on its own — there is no expiry to configure and no cleanup job that could fail to run. And because your project ID is in the input, the same browser visiting two different Coruve customers produces two unrelated hashes, so nobody can be followed from one site to another.

What the browser computes
SHA-256( projectId | userAgent | language | timezoneOffset | YYYY-MM-DD )

It is rebuilt on arrival, with a daily secret and the network address

What the browser sends is never what is stored. On arrival the server builds the identifier again, adding two things the browser cannot know: a secret derived from Coruve's own key and today's date, and the network address the request came from — trimmed to its network block for IPv6, so a phone rotating its address stays one visitor. Neither is kept. The secret is recomputed each day from a key that never leaves Coruve's infrastructure, and the address is used here and in the geography lookup and then discarded. So an attacker holding the event store cannot reproduce an identifier from a browser they control, cannot recover an address from one, and cannot link yesterday's identifiers to today's. Session identifiers are salted on the server the same way.

What is actually stored
SHA-256( projectId | HMAC(secret, "anon-daily:" + YYYY-MM-DD) | browserHash | networkBlock )

Why the network address is in there at all

Without it, the recipe above contains nothing that distinguishes one device from another — only the browser's user agent, its language and its timezone. Two different people on the same browser build, in the same language and timezone, produced the same identifier and were counted as one person. That undercounted visitors, most severely on audiences that are alike: one company, one country, one browser. Adding the coarse network address separates them without storing anything new, and without a cookie, a device fingerprint or a persistent identifier of any kind. The trade is stated below.

What this costs you, stated plainly

A returning visitor on a different day counts as a new visitor. That is the trade Coruve makes on purpose, and it is why the retention report is measured in weekly cohorts rather than by following individuals. Anyone offering both daily-unique visitors and long-term individual tracking is keeping a persistent identifier somewhere.

And what it still costs within a single day

The network address separates two devices on two networks, and it cannot separate two devices on ONE. Two people in one household, or two colleagues behind one office address, running the same browser build in the same language and timezone, still produce the same identifier for that day and are counted as one. People sharing one network and one browser set-up may still be counted once per day, so this can undercount. For IPv6 the address is trimmed to its network block on purpose — a phone that rotates its address during the day would otherwise become several visitors, which is the same error pointing the other way. This is the same limit Plausible and Fathom publish, and it is a much smaller one than the browser-only recipe had. We are telling you which way the number errs because a limit you cannot see is one you would otherwise mistake for precision.

Cookies and browser storage

No cookies at all, and exactly four keys in storage — three of them a visitor's.

The tracker sets no cookies

Not a first-party cookie, not a session cookie, not one that expires quickly. There is no call to document.cookie in the tracker. That is what puts Coruve outside the consent-banner requirement for analytics in most jurisdictions — but whether YOUR site needs a banner depends on everything else your site loads, and that is a question for your own counsel, not for this page.

What is in localStorage

Four keys, one per project, and none of them identifies a person. Three are written for an ordinary visitor; the fourth is a flag you set on your own browser, deliberately, and no visitor ever has. Everything else the tracker knows lives only for the length of a page view. The tracker no longer uses sessionStorage at all.

WhereWhatWhy it is thereBounds
localStorageThe session idSo a visit reads as one session rather than one per page — and, since tracker 1.3.0, one per browser rather than one per tab. A visitor with three tabs open is one session, not three.One value, expires after 30 minutes without activity
localStorageThe offline send bufferEvents that failed to send — a dropped connection, a tunnel, a laptop lid — are retried on the next page load instead of being lost.Discarded after 24 hours, at most 100 events, at most 64 KB
localStorageThe capture switches your dashboard setAdded in tracker 1.4.0. So the tracker knows what this project has switched on before its first batch answer comes back, without an extra network request at page load. It holds a version number and one 0 or 1 per switch — configuration of your project, never anything about the visitor holding it.One small JSON object, rewritten only when a switch changes
localStorageYour own exclude-my-visits flagSet by you, the site owner, on your own browser, so your testing does not land in your own reports.One flag, set and cleared by you
Three limits, whichever binds first: nothing older than 24 hours is ever retried, the newest 100 events are kept, and the whole buffer is capped at 64 KB. A visitor who never comes back leaves nothing behind that outlives a day.

One option turns all three visitor keys off

offlineBuffer: false in your snippet is the whole switch, and it is honest rather than partial: the buffer is cleared and never written again, the session id falls back to living in memory for the length of a tab, and the capture switches fall back to the compiled defaults on every page load rather than being cached. A site that set it stores nothing persistent on a visitor's device at all — which is the claim the option exists to make true, so it cannot be allowed to acquire a fourth key silently. Your own exclude-my-visits flag is the exception and is not governed by it, because it is not a visitor's key: it is set by your own deliberate action on your own browser.

Nothing persistent on the device
Coruve.init("proj_YOUR_ID", {
  apiHost: "https://ingest.coruve.com",
  offlineBuffer: false,
});

Do Not Track and Global Privacy Control

Both honoured, by not running at all.

The tracker disables itself

When the browser sends Do Not Track, or when Global Privacy Control is set, the tracker stops before it captures anything. There is no reduced mode and no anonymised mode — no page view, no event, no request to the ingest endpoint. Both signals are checked at initialisation and again on every capture, so a signal that appears mid-session is honoured mid-session.

The default — nothing to turn on
Coruve.init("proj_YOUR_ID", {
  apiHost: "https://ingest.coruve.com",
  // respectDNT is true by default. Setting it to false is a decision
  // you would have to make deliberately, and we do not recommend it.
});

What is never collected

The techniques Coruve does not use, the text it does not read, and the controls that decide the rest.

No fingerprinting

No canvas rendering, no WebGL probing, no font enumeration, no audio-context fingerprinting, no battery or hardware interrogation. The identifier's whole input is listed above — user agent, language, timezone offset — and that is deliberately weak entropy, because a strong fingerprint is exactly the persistent identifier this design exists to avoid.

No recordings, no screenshots, no keystrokes

Coruve does not record sessions, does not capture the screen, and does not observe what is typed. The Click & scroll maps are built from click coordinates and scroll depths on a grid — there is no image of your page anywhere in the system, which is why the report is not called a heatmap.

Element text is a switch, and passive clicks never carry it

For clicks on interactive elements, Coruve stores what was clicked as a structural descriptor — the tag, its id and its classes — and where it was clicked. Whether the element's text and its aria-label are read at all is a switch, and since tracker 1.4.0 it lives in Project Settings → Tracking rather than in your snippet: a project created from 1.4.0 onwards starts with it ON, and a project that existed before 1.4.0 starts with it OFF and is asked once before anything changes. The one unconditional veto is writing collectText: false in your own snippet — an explicit false there is never overruled by a dashboard switch. Whenever it is on, anything matching a personal-data pattern is redacted before it leaves the browser, the words are used to write a name and are then dropped — the row written to the event store carries the structural descriptor and nothing else, and there is no column for the text to be written into. Whenever it is off, the two fields that carry the text are deleted at ingest before the job that would process them is even created, which is a stronger promise than not storing them: they do not reach the worker at all. Clicks on non-interactive parts of the page, the ones that feed the click map, have no branch that can carry text at all: a click can land on a paragraph of prose, and there is no code path in which that prose is read.

Your own event properties are your responsibility

Coruve never puts personal data into a custom event — but you can. Properties you attach to your own tracked events are stored as you send them. Send an order id, not an email address.

Forms are observed, and the refusals are the point

Since tracker 1.4.0 a form submission is recorded by default, so this page has to say what that means rather than leave the impression that forms are untouched. What is recorded is that a form was sent, the form's structural descriptor, the path its action points at, and HOW MANY fields it had. Never a field's name and never a field's value: Coruve counts the fields, it does not read them. Nothing typed into a form reaches Coruve on this path or any other — there is no keystroke observation anywhere in the tracker, and an input, a textarea and a select are the three element types the text capture refuses to read from even when it is switched on.

Your own suppression, honoured before any Coruve setting

Put data-coruve-ignore on any element and nothing inside it produces an event of any kind — no click, no dead click, no form submission, no text. It is checked FIRST in both listeners, before the project's own settings are consulted, deliberately: a customer's suppression of their own markup must not be conditional on ours. On <body> it makes a page report its page view and nothing more. Every serious tracker ships one of these; this is the spelling of ours.

Six switches that stop collection rather than hide it

What Coruve is allowed to record is six on/off switches in Project Settings → Tracking: form submissions, outbound links, file downloads, contact links, using the words on buttons to name them, and counting #anchors as pages. Two properties make them worth calling a privacy control rather than a preference. First, a switch stops COLLECTION — it is not a read-time filter over data that is still being written, so there is nothing to un-hide later. Second, the five that govern what a row CONTAINS are enforced at ingest, server-side, on the way in: what a switch forbids is refused or deleted before the row is queued, whatever the browser tracker believes it is allowed to send. The tracker's copy of the switches only decides what leaves the page, which is the cheaper and more private half, never the authoritative one. (The sixth, counting #anchors as pages, is not a collection switch at all — it changes the URL a page view is reported under, so there is nothing at ingest for it to strip.) Turning a switch off takes effect on your visitors' next page view; what you already collected stays and keeps counting, and the change is marked on your charts so a step in a number has a reason beside it.

How long data is kept

A deletion promise enforced by the store, not by a cleanup script.

Retention by plan

Your plan's retention is stamped onto every event row at the moment it is written, and the event store's own time-to-live rule deletes the row when the window closes. It is not a filter over data that is still there — the row is gone. Which also means upgrading does not recover data that has already passed its window.

PlanEvent data kept for
Free30 days
Starter365 days
Pro730 days
Growth1,095 days
History you deliberately brought across from GA4 or Plausible is stored as daily aggregate counts carrying no visitor identifier of any kind, so it has no TTL. You delete it by deleting it — there is a control for exactly that in Settings.

Erasing history before the window closes

You do not have to wait for the clock. Project Settings → Privacy & data has an Erase history control: name a date and every event recorded before it is deleted, scoped to that project alone. It is the workspace OWNER's decision and nobody else's — an admin can change what is counted, only the owner can destroy what was — and a date in the future is refused, because erasing everything is what deleting the project is for and that asks you to type the name first. The confirmation says 'queued' rather than 'deleted' on purpose: the event store applies the deletion asynchronously, so reports can take a few hours to settle, and a past-tense claim that is still briefly false is not a sentence this page will print.

Where the data lives

Two places, and they hold different things.

Application and event store are separate

The Coruve application and its Postgres database — your account, your projects, your goals, your billing records — run on Railway in Amsterdam. Your visitors' analytics events live somewhere else entirely: a ClickHouse instance Coruve operates itself, on dedicated infrastructure in Frankfurt. Both sit in the EU, and the separation is still the point: the behavioural data is held in a store Coruve runs on its own machine, not alongside your account in a managed platform.

WhatWhereOperated by
Application, accounts, projects, billingAmsterdam, NetherlandsRailway (managed)
Analytics event storeFrankfurt, GermanyCoruve, self-hosted on Hostinger infrastructure

What a language model is shown

Two narrow paths, and a list of what is never in either.

Naming your events

Raw developer event names like btn_submit_final_v2 are sent in batches to Anthropic's Claude, together with the draft label Coruve's own heuristics produced, and it suggests a readable name and a category. Only the names and the drafts go — no properties, no visitor identifiers, no page URLs. Suggestions stay pending until you approve them.

Writing the sentence over a report

The Brief's cards, the Explain panel and Ask each put one sentence over numbers a deterministic engine already computed. The model is shown that computed answer — the metric name, the figures, the date window, and the row labels those figures belong to, which for a report about pages means page paths and for one about events means event names. It is shown nothing beyond what is already rendered on your screen underneath the sentence, and its output is checked against the answer before it is displayed. If the model is unavailable, a template sentence renders and not one number changes.

Never sent, in either path

Visitor identifiers, session identifiers, IP-derived location for an individual visit, your custom event properties, the text of a question you asked Ask, any note you wrote, your project id, your account, your organisation, and any row belonging to another project. Row labels crossing this boundary are treated as untrusted text and length-bounded on the way out — anyone can cause a name to be recorded on a website, so a name is never read as an instruction.

Turning it off

The model paths are optional at the infrastructure level: with no API key configured, labelling falls back to the heuristic name and every narrated surface renders its template sentence. The reports themselves are computed by ordinary queries and never depend on a model.

The documents that govern

This page explains how the system is built. Where the wording matters legally, these are authoritative: the privacy notice, the data processing addendum, and the cookie notice.

Privacy & data handling | Coruve