Self-hosted, cookie-free, GDPR-aligned web analytics. Drop one
<script> tag and get the panels you actually use: traffic, sources,
pages, devices, locations, hostnames, and Core Web Vitals.
Built with Next.js 15 (App Router), TypeScript, ClickHouse, Redis, and Tailwind CSS.
- No cookies, no
localStorage, no fingerprinting. - Daily-rotating session ID derived from a salted hash. Sessions cannot be linked across days.
- Tiny tracking script (~2 KB), no dependencies, defers loading,
honours
DNTandSec-GPC. - One snippet, many subdomains. Send
data-domain="example.com"on every subdomain and slice traffic by hostname in the dashboard. - Plausible-style breakdowns: Channels, Sources, Campaigns, Top Pages, Entry Pages, Exit Pages, Countries, Regions, Cities, Browsers, OS, Devices, Hostnames, UTM Campaigns.
- Click-to-filter everywhere. Filter state lives in the URL.
- Page detail drawer with sources, referrers, country split, device split, and a 24-bar hour-of-day distribution per page.
- 7x24 day-of-week heatmap.
- Optional Core Web Vitals (LCP, FCP, CLS, INP, TTFB) with web.dev good/needs-improvement/poor bucketing.
- Public read-only shared links per site.
- Bearer-token API for
/api/stats/*. - Customisable branding (title, logo, attribution toggle).
- Custom events via
window.chie(name, props?).
git clone https://github.com/olususus/Chie.git
cd Chie
cp .env.example .envEdit .env and set at minimum:
AUTH_SECRET- a 64-character random stringSALT_SECRET- a 32-character random stringAPP_URL- the public URL of your instance (e.g.https://analytics.example.com)
Then start everything:
docker compose up -dOpen http://localhost:3000 to create the admin account.
Add this to the <head> of every page you want to track:
<script defer data-domain="example.com" src="https://your-instance.com/t.js"></script>Optional attributes:
| Attribute | Description |
|---|---|
data-domain |
Your registered site domain (required) |
data-api |
Proxy the event POST through your own origin |
data-exclude |
Comma-separated path globs to exclude client-side |
data-track-perf |
Set to "true" to collect Core Web Vitals |
data-allow-localhost |
Set to allow tracking on localhost |
window.chie("signup", { plan: "pro", source: "landing" });Up to 20 props per event. Keys are capped at 40 characters, values at 200.
| Topic | Link |
|---|---|
| Installation | docs/install.md |
| Configuration | docs/configure.md |
| Tracking script | docs/script.md |
| API reference | docs/api.md |
| Tutorial | docs/tutorial.md |
| Security model | docs/security.md |
| Security audit | docs/security-audit.md |
| Migration from TinyTracker | docs/migrate-tinytracker.md |
Browser --> t.js (2 KB, no deps)
|
v
POST /api/event
|
v
+-------------------+ +------------+
| Next.js (Node) | ----> | ClickHouse |
| App Router API | | (events + |
| + Dashboard UI | | metadata) |
+-------------------+ +------------+
|
v
+----------+
| Redis |
| (live, |
| rate, |
| sessions)|
+----------+
- ClickHouse holds the events table, materialized views for daily
aggregates, and user/site/API key metadata in
ReplacingMergeTreetables. - Redis powers live visitor counters (HyperLogLog over rolling 5-minute windows), per-IP rate limiting, and same-day session deduplication.
- Next.js hosts both the ingestion API and the dashboard UI from a single process.
See docs/configure.md for the full list. Key ones:
| Variable | Required | Description |
|---|---|---|
AUTH_SECRET |
Yes | 64-char secret for JWT signing |
SALT_SECRET |
Yes | 32-char secret for session hashing |
DATABASE_URL |
Yes | ClickHouse HTTP URL |
REDIS_URL |
Yes | Redis connection string |
MAX_SITES_PER_USER |
No | Default 50 |
DATA_RETENTION_MONTHS |
No | Default 24 |
STORE_CITY |
No | Default false |
ALLOW_SIGNUP |
No | Set true for open registration |
All stats endpoints live under /api/stats/* and accept either:
- A session cookie (from the dashboard), or
- An
Authorization: Bearer tt_...header (API key), or - A
?shared=SLUGparameter (public shared link, read-only)
Endpoints:
GET /api/stats/aggregate- totals for a periodGET /api/stats/timeseries- time-bucketed seriesGET /api/stats/breakdown- top N by propertyGET /api/stats/hourly- 7x24 heatmap dataGET /api/stats/perf- Core Web Vitals percentilesGET /api/stats/realtime- current live visitors
All accept ?domain=, ?period=, ?from=, ?to=, and f_* filter params.
- Passwords hashed with bcrypt (cost 12)
- Constant-time login (dummy compare on user-not-found)
- Rate limiting on all auth endpoints
- Parameterized ClickHouse queries (no string interpolation)
- Zod validation on every request body
- Security headers on all dashboard responses (HSTS, X-Frame-Options, nosniff, Permissions-Policy)
- Tracker sends with
credentials: "omit"(no cookies ever) - Generic error messages that do not leak user enumeration
npm install
npm run devRequires ClickHouse and Redis running locally (or use docker compose for just the backing services).
npm run typecheck # TypeScript check
npm run lint # ESLint
npm run build # Production build- Fork the repo
- Create a feature branch
- Make your changes
- Run
npm run typecheckandnpm run lint - Open a pull request
Please keep PRs focused on a single change.