seyr
Cookieless, no-PII web analytics with a sub-1KB tracker, a columnar event store, and multi-tenant billing — a self-hostable alternative to Google Analytics.
The problem
Site owners want per-site traffic insight without cookie banners, consent friction, or handing visitor data to a third party. seyr delivers fast aggregate dashboards while storing no cookies and no personal data — GDPR/CCPA-friendly by design — for small-to-mid operators and multi-site agencies.
Challenges
Identifying visitors without cookies or PII
With no durable client id and raw IP/UA both counting as PII, the Go ingestor derives visitor_id = hash64(dailySalt | ip | ua | domain), consuming IP and UA transiently to compute geo/device then discarding them. The salt rotates every UTC day so a visitor cannot be linked across days, and folding the domain into the hash blocks cross-site correlation.
High-volume events with instant aggregate reads
Every pageview and event lands in ClickHouse as a partitioned MergeTree using LowCardinality/FixedString encodings and a TTL retention ceiling. A daily AggregatingMergeTree rollup and materialized view pre-aggregate pageviews and uniqState(visitor_id), so unfiltered date ranges skip the raw table entirely.
A sub-1KB tracker that still handles SPAs
The embeddable script is built by esbuild to an IIFE with a build-time size guard, importing only a zod-free config subpath. It uses navigator.sendBeacon with a fetch keepalive fallback, patches history.pushState/replaceState for SPA pageviews, honors DNT, and posts to a neutral /i path whose filename avoids ad-blocker trigger words.
Per-tenant quota enforcement at ingest speed
Beacons must resolve to a tenant and count against a monthly quota without touching Postgres on the hot path. A TTL cache resolves domain→site/org/limit (with negative caching of unknown domains) and an in-memory month-to-date counter — seeded from Postgres, flushed as deltas — keeps limit checks O(1), with soft/block modes for over-limit traffic.
Implementation
Decoupled Go ingest path
POST /i always replies 202 (never leaking which UAs or domains are filtered), runs a two-layer bot filter, validates, resolves the site, records usage, and hands the row to a single-goroutine batcher. The buffer flushes by size or interval and drops-and-counts on saturation, so a slow ClickHouse never stalls request latency.
Parameterized read path with rollup fallback
The dashboard maps a whitelist of filter keys to columns and binds every value as a ClickHouse query parameter — no user input is ever interpolated into SQL. Unfiltered daily ranges are served from the events_daily rollup via uniqMerge, falling back to raw events for hourly/filtered queries with WITH FILL gap-zeroing.
Auth and tenancy in SvelteKit
Sessions store a high-entropy token client-side while the DB persists only its SHA-256 (the raw token is never stored), with sliding renewal and argon2 passwords. Signup atomically creates the user, first org, and owner membership; every query is org-scoped and cross-org access 404s.
Provider-agnostic billing with idempotent callbacks
A PaymentProvider interface fronts an SSLCommerz adapter (cards + bKash/Nagad/Rocket) and a mock adapter for dev. Gateway redirects are handled session-independently and idempotently — a finalized payment short-circuits — before activating the subscription and optionally storing a tokenized card for auto-renew.
Why this stack
- Go
- Cheap goroutines and channels make the single-owner in-memory batcher and drop-on-saturation design natural for the write-hot beacon endpoint, shipping as a lean static binary.
- ClickHouse
- A columnar OLAP store so high-volume events compress well and aggregate queries over millions of rows stay fast, with materialized-view rollups for time series.
- PostgreSQL
- The transactional source of truth for users, orgs, sites, subscriptions, and usage counters — data that needs referential integrity.
- SvelteKit
- One SSR app covering dashboard, marketing, auth, billing, and the read API, deployed via the Node adapter for self-hosting.
- esbuild
- Minifies and bundles the tracker to an IIFE under 1KB with a build-time size guard.
- Turborepo
- A pnpm monorepo so a schema change touches the ClickHouse DDL, the Go insert, and the TypeScript query in one commit.
What it does
- Cookieless, no-PII tracking with daily-rotating visitor hashing and a sub-1KB SPA-aware script
- Dashboards for visitors, pageviews, bounce rate, and duration with page/source/country/browser/OS/device breakdowns
- Custom event tracking with key/value props surfaced as ranked conversions
- Multi-tenant orgs with roles and public shareable dashboards via token
- Plan-based monthly event limits enforced at ingest (soft/block modes)
- SSLCommerz billing with idempotent callbacks, tokenized auto-renew, and bot/AI-scraper filtering
Get in touch
Building something interesting? Let's talk.
Whether it's a hard distributed-systems problem, a platform that needs to scale, or just a second opinion — I'm generally up for it.