Privacy & compliance
Canopy's privacy story is not a policy layered on top of tracking — it is the architecture. The tracker stores nothing in the browser, sends nothing to third parties, and the server never persists a raw IP address. This page documents exactly what is collected, what is engineered out, and what that means for GDPR and consent banners. It is documentation, not legal advice — you own your compliance posture.
1What is collected
Every event stores exactly these fields — no more:
| Field | Purpose | Notes |
|---|---|---|
| Event name | What happened | pageview, outbound, identify, or your custom events. |
| URL, path, title | Which page | Captured from the browser at send time. |
| Referrer | Where the visitor came from | Empty for direct traffic. |
| Viewport width | Device class | Mobile vs desktop — no full device profile. |
| Session ID | Grouping events into visits | A random in-memory UUID — see below. |
| IP hash | Distinguishing visitors | Salted SHA-256, truncated to 16 hex chars. |
| Country | Rough geography | From CDN headers (Vercel/Cloudflare) when present. |
| Properties | Your custom data | Whatever your events attach, stored as JSONB. |
2What is engineered out
| Never collected | How |
|---|---|
| Cookies | The tracker writes nothing to the browser — no cookies, no localStorage, no IndexedDB. |
| Raw IP addresses | The server hashes the IP with SHA-256 and a secret salt, keeps 16 hex chars, and discards the original. |
| Fingerprints | No canvas, audio, font, or device fingerprinting — the data to build one is never gathered. |
| Cross-site tracking | Events post to your domain about your pages. Canopy cannot see visitors on other sites. |
| Personal details by default | No names, emails, or accounts — unless you explicitly send them in identify or event props. |
3The session model
When the first event of a visit is sent, the tracker generates a random UUID and keeps it in memory. Every event in that page's lifetime carries the same session ID, so SPA navigations group into one visit. When the tab closes, the session is gone — nothing was ever written down.
page load ──► pageview ──► SPA route ──► pageview ──► tab closed
└──────────── same session ID ────────────┘ ✕ goneThis is why Canopy needs no consent banner in most jurisdictions: there is no storage to consent to, and no identifier that outlives the page. On logout, canopy.reset() discards the session so the next activity starts fresh.
4IP hashing in detail
The ingestion endpoint extracts the visitor IP from standard proxy headers (x-forwarded-for, then x-real-ip), combines it with your server-side salt, and stores only:
ip_hash = sha256(ip + CANOPY_IP_SALT).slice(0, 16)
The salt (CANOPY_IP_SALT) never leaves your server, so the stored value is useless to anyone who obtains the database without the salt — a preimage attack would require guessing the entire salt. Keep the salt stable if you rely on returning-visitor counts: rotating it changes every hash from that point forward. And set a strong value in production; the built-in default exists only for local development.
5Do Not Track
The tracker checks the browser's Do Not Track signal (navigator.doNotTrack and its legacy variants) on every send and drops all events when it is enabled — before any network request is made. This is the default and needs no configuration. Overriding it is possible but not recommended:
<script src="/p.js" data-site-id="canopy_example_com" data-respect-dnt="false" async></script>
6What this means for compliance
GDPR
Canopy collects no special-category data, stores nothing on the visitor's device, and pseudonymizes the IP before storage — the combination most privacy regulators treat as out of scope for consent-banner requirements for cookieless analytics. Because all data lives in your own Postgres database, you can fulfill access and deletion requests directly with SQL, and the export endpoint gives visitors their data on request.
Self-hosting
Self-hosting moves data residency entirely into your hands: choose a region, choose a provider, keep the database in the EU if that is what your audience requires. The dashboard is the only component with accounts, and analytics events are never tied to them — events know a site, a session, and a hash.
beforeSend hook, and keep your CANOPY_IP_SALT secret. The events guide shows the hook.