Cookieless, privacy-first web analytics — on the edge.
A simple, fast, GDPR-friendly alternative to Google Analytics. The tracker is < 2 KB, it sets no cookies and stores no personal data, so most sites need no consent banner. The whole stack — tracker, collector, dashboard, and API — runs serverless on Cloudflare Workers + D1, so you can self-host it for free.
- Cookieless. Counts daily unique visitors with a salted hash of IP + User‑Agent that rotates at midnight UTC. No cookies, no IP stored, no fingerprinting — usually no consent banner needed.
- Tiny & fast. The tracker is under 2 KB, async, with zero dependencies. No Core Web Vitals hit.
- The metrics that matter. Pageviews, unique visitors, sessions, top pages, referrers, countries, devices, time‑on‑page (counted only while the tab is visible) and scroll depth, plus outbound‑link clicks.
- Multi‑tenant. Register with email/password or Google, claim the domains you own, and see only your data.
- Per‑site sharing. Invite teammates by email (with optional automatic emails via Resend) to view a specific site, read‑only.
- Super‑admin view. A designated account sees every tracked domain plus an "All sites (combined)" aggregate — built for running 100s of sites.
- Fast at scale. Headline numbers read from nightly per‑day rollups; today stays real‑time.
- Self‑hostable & open source. Deploy to your own Cloudflare account; your data never leaves it.
- Cloudflare Workers — single Worker serves the tracker, collector, stats API, marketing site, and dashboard.
- Cloudflare D1 — SQLite at the edge for storage.
- TypeScript, no framework. Dashboard is vanilla JS + Chart.js.
- Cron Triggers for the nightly rollup. Web Crypto for password hashing (PBKDF2‑SHA256). Resend (optional) for invite emails.
You'll need a Cloudflare account and Node 18+.
git clone https://github.com/xolqy-com/xolqy.git
cd xolqy
npm install
# Create the D1 database and paste the returned database_id into wrangler.toml
npx wrangler d1 create xolqy
# Create the tables
npx wrangler d1 execute xolqy --remote --file=schema.sql
# Set the required secrets
npx wrangler secret put SALT_SEED # any long random string
# Deploy
npx wrangler deployThen point your domain at the Worker (Custom Domain in the dashboard) and add the snippet to any site you want to track:
<script defer src="https://your-domain.com/t.js"></script>Register an account at /login, add your domain, and traffic shows up live.
Set with npx wrangler secret put <NAME> (and mirror them in a gitignored .dev.vars for wrangler dev):
| Name | Required | Purpose |
|---|---|---|
SALT_SEED |
✅ | Seed for the daily visitor‑hash salt. |
SUPERADMIN_EMAIL |
– | Comma‑separated emails granted the all‑sites super‑admin view. |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
– | Enable "Sign in with Google" (OAuth 2.0). |
GOOGLE_REDIRECT_URI |
– | Override the callback (defaults to <origin>/auth/google/callback). |
RESEND_API_KEY |
– | Auto‑send invite emails via Resend. If unset, invite links are still generated to share manually. |
INVITE_FROM |
– | From address for invites, e.g. Xolqy <invites@your-domain.com> (domain must be verified in Resend). |
- Google Cloud Console → Create credentials → OAuth client ID → Web application.
- Authorized redirect URI:
https://your-domain.com/auth/google/callback(andhttp://localhost:8787/auth/google/callbackfor dev). npx wrangler secret put GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET. Until set, the button shows a friendly "not configured" notice and email/password still works.
Cookieless identity. Instead of a cookie, the collector computes sha256(IP + User-Agent + SALT_SEED + YYYYMMDD) and keeps only the first 12 hex chars. The day component rotates the value every midnight UTC, so it can't track anyone over time. The raw IP is never stored.
Rollups for scale. A Cron Trigger (00:15 UTC) aggregates each completed day into daily_rollups (one row per site per day) via src/rollup.ts. The dashboard's KPIs and chart read pre‑aggregated totals for past days and query raw events only for today — so history is cheap and today is live. Averages are stored as sum + count so they recombine across any range; because the visitor hash rotates daily, summing per‑day uniques equals the range‑wide distinct count.
Subresource Integrity. /t.js is the rolling latest (short cache). For production, load the frozen, versioned build /v1/t.js — immutable cache + CORS — with an integrity hash so the browser refuses to run it if a single byte changes:
<script defer
src="https://xolqy.com/v1/t.js"
integrity="sha384-pKXxF1rkM+EtLGxb5cqWrj0csxQ//a4DJH4lEGqHbUql1i5GSyVZgBxs4j+ljjOo"
crossorigin="anonymous"></script>A breaking change ships as /v2/t.js with a new hash; pinned tags keep working. Recompute the hash with curl -s https://your-domain.com/v1/t.js | openssl dgst -sha384 -binary | openssl base64 -A.
Generating OG images. npm run og renders a per‑page Open Graph PNG into public/og/ (uses sharp). Re‑run after adding pages or posts.
xolqy/
├── wrangler.toml Cloudflare config (Worker, D1 binding, cron trigger)
├── schema.sql D1 tables: events, clicks, users, sessions, sites, daily_rollups, shares
├── scripts/build-og.mjs Per-page Open Graph image generator
├── src/
│ ├── worker.ts Entry: routing, collector, stats/auth/sharing API, cron, canonical host
│ ├── auth.ts Accounts, sessions, password hashing, Google OAuth, sharing/invites
│ ├── stats.ts Aggregation queries (rollup + live-today hybrid)
│ ├── rollup.ts Nightly events → daily_rollups job
│ ├── site.ts Marketing site shell, pages, blog, sitemap/robots/llms.txt/RSS, invite page
│ └── tracker.ts Source of the < 2 KB tracker served at /t.js
└── public/
├── login.html Login / register (email/password + Google)
├── dashboard.html Stats dashboard (vanilla JS + Chart.js)
├── dashboard.css
└── og/ Generated Open Graph images
- IP addresses are never stored. Only a short, daily‑rotating salted hash is kept — enough to count daily uniques, not enough to identify or track a person.
- No cookies, no
localStoragepersistence beyond a per‑tab session id. - No third‑party data sharing and no fingerprinting beyond the daily hash.
Issues and PRs welcome. Run npm run dev for a local Worker, and npx tsc --noEmit to type‑check.
AGPL-3.0 © Xolqy — if you run a modified version as a network service, you must publish your changes.