IndieStats.

Privacy-friendly analytics on your own infrastructure.

Self-hosted Laravel analytics: real-time dashboards, advanced filters, and localized exports — multiple sites, no cookies, no mandatory consent banners.

Features

Everything you need in one place — without handing visitor data to third parties or weighing down the sites you track.

Pageviews & context
localStorage visitor IDs (no cookies), time on page, document.title, query string, language, timezone, search terms, and bot detection.
Real-time
Active visitors, last-5-minute pageviews, a 30-minute activity chart, and a live feed — on the dashboard and per site.
Nine analytics tabs
Summary, Real-time, Content, Traffic, UTM, Technology, Geography, Visitor, and Events — each with server-side DataTables.
Advanced filters
Tom Select type-ahead across source, path, title, UTM, device, country, ASN, visitor, and more — applied to every tab and export.
Geography & ASN
Choropleth country map via MaxMind GeoLite2. ISP / hosting provider via DB-IP ASN Lite.
Events & outbound
Custom events with up to 20 properties, goals, and clicks to external domains.
Localized export
Background XLSX jobs with sheet titles and headers in IT, EN, DE, FR, or ES.
Multi-site
Multiple sites per account, domain allowlists, Cloudflare-aware client IP, noscript pixel, and scheduled pruning.

Screenshots

From the multi-site overview down to the snippet you paste on your pages. Every screenshot comes from the built-in demo data.

IndieStats dashboard with real-time activity across all sites and a trend for each site
Multi-site overview — live visitors across sites, then unique visitors and trends for each one.
Per-site summary with visitors, pageviews, time on page, outbound clicks, top pages and top sources
Per-site summary — headline metrics, top pages, top sources and the daily trend.
Real-time tab with active visitors, pageviews in the last five minutes and a live activity feed
Real-time — active visitors, a 30-minute chart and the pages being viewed right now.
Filter panel with source and device filters applied
Filters — 21 dimensions with type-ahead search, applied to every tab, table and export.
Geography tab with a world map shaded by visits
Geography — a choropleth map and country breakdown in the user's language.
Events tab with goals for purchases, add to cart, subscriptions and newsletter signups
Events & goals — custom events with properties, and goals to follow conversions.
New site confirmation with the tracking snippet ready to copy
Sites & snippets — add a site, set allowed domains, and copy the tracking code.
Dashboard and site summary on a phone
Responsive — the dashboard and every site tab work on a phone.

Install

One command. PHP 8.4+, Composer, and Node 20+. SQLite by default — MySQL and PostgreSQL work too.

git clone https://github.com/andreapollastri/indiestats.git
cd indiestats
composer run setup

The setup script installs dependencies, runs the Vite build, creates .env, generates the app key, and runs migrations.

Manual install

composer install
npm install
npm run build
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrate
php artisan db:seed

Quick start

From zero to your first metrics in three steps.

01

Install & run

Clone the repo, run composer run setup and php artisan db:seed to create the first admin (admin@users.test / password — change it), then serve the app with php artisan serve or Laravel Herd.

02

Create a site & paste the snippet

Add a site with a name and allowed domains. Copy the async script and noscript fallback before the closing body tag — the base URL is your APP_URL.

03

Open the dashboard

Multi-site overview with live visitor counts, then nine per-site tabs — all narrowed by the same filters and exportable to localized Excel.

Demo

Explore every screen without installing the snippet anywhere: one command builds a realistic year of traffic.

php artisan migrate:fresh --seed --seeder=DemoDataSeeder

Sign in as admin@users.test / password. The demo creates five fictional sites — a developer blog, open-source docs, a coffee shop, a SaaS landing page and a design studio — with about 230,000 pageviews, growth and seasonality, returning visitors, UTM campaigns, a Hacker News spike, purchase funnels, crawlers, a small team with roles and live activity. It takes about 10 seconds on SQLite and only uses .example domains.

Real-time views show the last few minutes. Refresh them right before a demo:

php artisan db:seed --class=DemoLiveTrafficSeeder
Safe by default: both demo seeders refuse to run when APP_ENV=production, and re-running the demo replaces only the demo sites.

Tracking

A lightweight async snippet plus a 1×1 pixel for visitors without JavaScript.

<script async src="https://your-indiestats-url/i/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.js"></script>
<noscript><img src="https://your-indiestats-url/collect/pixel.gif?k=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&p=/" width="1" height="1" /></noscript>

The script tracks pageviews, time on page, outbound clicks, UTM parameters, on-site search queries, page title, query string, language, timezone, and a per-tab session ID.

Custom events

indiestats.track('signup');

indiestats.track('purchase', {
  plan: 'pro',
  price: '29.99',
  currency: 'USD'
});

Up to 20 key–value properties per event. Tie events to goals in the dashboard to watch conversions.

Allowlist: only domains listed on the site can send data (validated via Origin / Referer). Always set allowed domains in production.

Dashboard

A multi-site overview, then dedicated tabs for every angle of traffic.

  • Overview — unique visitors, pageviews, sparklines, live counts, and date ranges (today → 1 year).
  • Summary — headline metrics, top pages, and top sources.
  • Real-time — active visitors, last 5 minutes, 30-minute chart, live activity feed.
  • Content / Traffic / UTM — paths, titles, referrers, search queries, and campaigns.
  • Technology — device, browser, OS, language, timezone, and ASN.
  • Geography — country table plus interactive choropleth map.
  • Visitor / Events — visitor IDs, human vs bot, custom events and goals.

UI is localized in IT, EN, DE, FR, and ES — including DataTables, country names, and export sheets.

Filters

One filter bar. Every tab, chart, realtime count, DataTables AJAX call, and Excel export respects it.

Filter Description
Source Referrer / engine
Search term On-site search query
Page / Page title Path and document.title
Query string Full landing-page query
UTM (5) source, medium, campaign, term, content
Event Custom event name
Device / Browser / OS From User-Agent
Language / Timezone From the tracker script
Visitor / type Persistent ID · human vs bot
Country / ASN GeoIP and network

Production

Deploy without surprises. Set APP_URL to HTTPS, build assets, run the scheduler and a queue worker.

npm run build
php artisan config:cache
php artisan route:cache
php artisan view:cache
Variable Notes
APP_URL Required. Public HTTPS URL used by the tracker snippet.
APP_ENV / APP_DEBUG production / false
QUEUE_CONNECTION Needed for Excel exports (default database)
ANALYTICS_RETENTION_DAYS Raw data pruning window (default 375)
GEOIP_* Optional MaxMind / ASN database paths and license key
Proxies: behind Cloudflare, CF-Connecting-IP is used for GeoIP and ASN. For other reverse proxies, configure Laravel trusted proxies.

Scheduler

One cron entry drives backups, pruning, weekly GeoIP updates, monthly ASN downloads, and self-updates.

* * * * * cd /path-to-indiestats && php artisan schedule:run >> /dev/null 2>&1
Command Schedule Purpose
backup:run Daily 00:00 Back up the database, .env and storage/app to BACKUP_DISK
backup:clean Daily 01:00 Remove old backups
analytics:prune Daily 02:00 Remove raw pageviews, events, and outbound clicks past retention
geoip:update Mondays 04:15 Download GeoLite2-Country when a MaxMind key is set
dbip-asn:update 3rd of month 04:30 Download DB-IP ASN Lite
app:update pending Every minute Start the update an admin asked for on the Updates page
app:update apply --auto Daily at APP_UPDATE_TIME (04:00) Install the newest build of the channel, while automatic updates are on

Queue

Excel exports run as background jobs. Locally, run php artisan queue:listen next to the server. In production, keep one running:

php artisan queue:work --sleep=3 --tries=3

Use Supervisor (or equivalent) so the worker restarts automatically.

Updates

A production installation updates itself from GitHub, from the scheduler.

Admins manage it on the Updates page: the installed release, the channel, the nightly update (APP_UPDATE_TIME, 04:00) and an update on demand, which starts within a minute. The cron entry is required.

  • Stable installs the release tags (1.2.3; v1.2.3 and 1.2 count too); Beta follows the head of main.
  • An update never goes backwards. Force update installs the channel's newest build again (or one whose history differs), never an older one.
  • Each update backs up the database (storage/app/updates/backups, the newest 5 are kept), installs the build, runs composer install --no-dev (assets are committed, so no Node), migrates, refreshes the caches, restarts the queue workers and checks that IndieStats boots. If a step fails, the previous code and the database come back; the log is in storage/app/updates/update.log.
  • A git checkout is updated in place; a releases deploy (Cipi, Deployer) gets a new releases/<build> next to the running one, sharing its .env and storage, and current is switched to it.
php artisan app:update status          # the last check and the last run (JSON)
php artisan app:update check           # what the channel would install
php artisan app:update apply [--force] # update now
php artisan app:update channel stable  # or beta
php artisan app:update auto off        # or on

The scheduler's user needs git, composer and the database client. Settings: APP_UPDATES (on in production), APP_UPDATE_REPOSITORY, APP_UPDATE_CHANNEL, APP_UPDATE_AUTO, APP_UPDATE_TIME, APP_UPDATE_BACKUP, APP_UPDATE_ASSETS, and APP_UPDATE_PHP, APP_UPDATE_COMPOSER, APP_UPDATE_NPM, APP_UPDATE_GIT for tools missing from its PATH. Installations from before 1.0.0 need one last manual update (git pull or cipi deploy).

Deploy with Cipi

Run IndieStats on any Ubuntu VPS with cipi.sh — PHP-FPM, MariaDB, Nginx, Let's Encrypt, zero-downtime releases, and queue workers.

Updates work there too: IndieStats builds the new release next to the running one, switches current to it and restarts Cipi's workers. A later cipi deploy keeps working as before.

Explore cipi.sh →