Analytics without the noise.
Simple, privacy-friendly analytics for solo developers and small products.
Screenshot of the dashboard with the synthetic development seed data. Not real traffic.
Most analytics tools give you hundreds of metrics. You usually need a few. Tinylytics answers the questions a solo developer actually asks:
- How many people visited?
- What did they view?
- Where did they come from?
- What devices are they using?
- Where are they located?
- Is traffic growing or falling?
One small script, one screen, a few seconds.
- Visitors, page views, sessions, bounce rate with a traffic chart for today, 7, 30 or 90 days
- Top pages, sources (Google, GitHub, Hacker News…), countries, devices, browsers and OS
- Single-page app support: History API navigation is tracked, duplicates are dropped
- Privacy by design: no cookies, no personal data, no raw IP addresses stored
- Tiny tracker: about 1 KB gzipped, zero dependencies, never blocks your site, fails silently
- Multiple sites per account, with install instructions and automatic "tracking installed" verification
- Row Level Security: every read is scoped to the owner by PostgreSQL itself
- Open source (MIT) and self-hostable on Supabase and any Next.js host
Not included, on purpose: session recordings, heatmaps, funnels, user profiles, fingerprinting, A/B testing, AI reports.
Tinylytics dashboard ── Next.js App Router, reads via RLS-protected SQL functions
│
▼
Analytics API ── POST /api/collect (Next.js route handler)
│ validate → normalize → hash visitor → one SQL call
▼
PostgreSQL (Supabase) ── events, sessions, daily rollups, auth
▲
│
Tracking script ── /tracker.js, sendBeacon, no cookies
▲
│
Your website
| Directory | What's inside |
|---|---|
web/ |
Next.js app: landing page, dashboard, docs, /api/collect, serves /tracker.js |
tracker/ |
The tracking script (plain JavaScript, built with esbuild into web/public/tracker.js) |
supabase/ |
Migrations, development seed data and database tests |
docs/ |
Documentation, also rendered at /docs |
Each page view is a single database round trip: tinylytics_ingest() stores the event, updates the session and increments daily rollups in one transaction. The dashboard reads the rollups, so a 90-day view is as cheap as a 1-day view. No Redis, queues or ClickHouse required.
- The tracker sends the page path (never the query string), the external referrer and a campaign source. Nothing else.
- IP address and user agent are used in memory to derive a visitor hash, browser family, OS, device class and country — then discarded.
- Visitors are identified by
sha256(daily random salt ‖ site ‖ sha256(ip ‖ user agent)), truncated. Salts are deleted after about two days, so hashes can't be reversed or linked across days or sites. - Unique visitors are an estimate: a person is counted once per day per site.
Read the full privacy model, including its limitations.
Requirements: Node.js 20.9+ (22 recommended), Docker (for the local Supabase stack).
git clone https://github.com/App-Chef/Tinylytics.git
cd Tinylytics
npm install
npx supabase start # Postgres + Auth locally; applies migrations and seed data
cp .env.example web/.env.local # paste the API URL, anon key and service_role key it printed
npm run dev # http://localhost:3000Sign in with the demo account created by supabase/seed.sql:
- Email:
demo@tinylytics.local - Password:
tinylytics-demo
It owns a site with 90 days of synthetic traffic, clearly labelled as demo data, plus an empty site that shows the "waiting for your first event" state. Local emails (magic links, confirmations) are caught by the local email viewer at http://localhost:54324.
| Command | Does |
|---|---|
npm run dev |
Build the tracker and start Next.js in development |
npm run build |
Build the tracker, then the Next.js app |
npm run lint / npm run typecheck |
ESLint / TypeScript |
npm test |
Tracker tests (jsdom) and web unit/component tests (Vitest) |
npm run test:db |
Database tests: ingestion, sessions, visitors, rollups, RLS and cross-user access |
npm run test:db creates a throwaway PostgreSQL cluster when initdb is available, or runs against DATABASE_URL (e.g. postgresql://postgres:postgres@127.0.0.1:54322/postgres for supabase start).
| Variable | Browser | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
yes | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
yes | Public anon key (access is limited by RLS) |
SUPABASE_SERVICE_ROLE_KEY |
no | Server only: event ingestion, retention job, account deletion |
NEXT_PUBLIC_SITE_URL |
yes | Public URL of the deployment, used in the snippet and auth redirects |
NEXT_PUBLIC_AUTH_GOOGLE_ENABLED |
yes | true to show Google sign-in |
CRON_SECRET |
no | Protects GET /api/cron/prune |
DATA_RETENTION_DAYS |
no | Raw event retention, default 395. Daily totals are kept. |
See .env.example and Configuration.
- Create a project at supabase.com.
- Apply the schema:
npx supabase link --project-ref <ref> && npx supabase db push. - Authentication → URL Configuration: set the Site URL to your deployment and add
https://<your-domain>/auth/callbackto the redirect URLs. - Optional: enable the Google provider, and configure SMTP for production email.
Never run supabase/seed.sql against production.
<script defer src="https://your-tinylytics-host/tracker.js" data-site="YOUR_SITE_ID"></script>Add it before </head>. Next.js, SPA, hash-router and WordPress instructions are shown in the app under Tracking, and in Installation. Options: data-hash, data-respect-dnt, data-allow-localhost, data-api — see Tracking script.
Tinylytics runs on a Supabase project plus any Node.js host (Vercel, a VPS, Docker, Fly.io…):
npm ci
npm run build
npm run start -w webCountry data comes from your platform's geo headers (Vercel, Cloudflare, CloudFront, or X-Country-Code from your own proxy). A daily retention job deletes old raw events. Full guide: Self-hosting.
Introduction · Installation · Tracking script · Events · Dashboard · Privacy · Self-hosting · Configuration · API · Contributing
Contributions that make Tinylytics more accurate, private, fast or clear are very welcome. Please read CONTRIBUTING.md and open an issue before starting on a new feature. Security issues: see SECURITY.md.
