# traffic-source: cookie-free analytics you host yourself

> traffic-source is a self-hosted web analytics dashboard built with Next.js and SQLite that tracks pageviews, referrers and affiliate referrals without cookies. Its distinguishing features are Stripe conversion tracking by polling, an affiliate commission system, and a search console integration, all in one Node process you can run on a small VPS.

**mddanishyusuf/traffic-source** — Open-source web analytics you deploy in one click. Track pageviews, referrers, and conversions — no cookies, fully self-hosted. The cloud version is also live ⬇️

- Repository: https://github.com/mddanishyusuf/traffic-source
- Website: https://piqo.app
- Stars: 672 · Forks: 88
- Language: JavaScript
- License: not declared
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/mddanishyusuf-traffic-source

## Analytics that stays on the box it runs on

Most web analytics products ask you to accept a cookie banner and hand visitor data to someone else's servers. traffic-source takes the opposite position. It is a self-hosted dashboard that stores everything in a local SQLite file, and the README lists the promise plainly: no cookies for tracking, and all data stays on your server.

The feature list is broader than pageview counting. It covers referrers and UTM parameters, country and city level geo data, browser and device breakdowns, and multi-site tracking from a single dashboard. There is a conversion tracker tied to Stripe, an affiliate system with custom commission rates and referral links, and a visitor journeys view that replays the pages a visitor saw before converting.

The audience is a site owner who already runs at least one server and is uncomfortable with a third-party script reading every page. The README suggests a $4 per month VPS as a realistic target, and the deploy story is a Node application behind Nginx, which is about as simple as self-hosted analytics gets.

One detail deserves attention early: the description links to a hosted cloud version alongside the open source project, so this is a project with a commercial interest behind it, and the code and the hosted service may not move together.

## One Next.js process over a SQLite file in WAL mode

The stack is conventional for a self-hosted dashboard. The framework is Next.js 16 with React 19, the database is SQLite through better-sqlite3 running in WAL mode, styling is SASS and charts are Recharts. Payments talk to the Stripe API by polling rather than by webhook, and authentication uses JWT with httpOnly cookies, with bcryptjs in the dependency list for password hashing.

The dependency set in package.json also shows leaflet and react-leaflet for maps, react-flagpack for country flags, ua-parser-js for user agent parsing, and the AWS SDK client for S3, which is what a self-hosted product would need for storing exported reports or backups somewhere other than the disk.

Because the whole thing is one Node process with a file-backed database, the operational model is simple but also the constraint. SQLite in WAL mode handles a single writer well, and the README advertises a zero-config database with no external services. That is exactly right for a small or medium site, and exactly wrong for a high-traffic site that would need concurrent write throughput. The app runs on port 3000 by default, and the package.json declares Node 20 or greater as the minimum.

## Clone, copy the env file, then build and start

The self-host path is the longer of the two documented options, the other being a one-click deploy on Railway. For the VPS route the README asks for Node.js 20+, a small VPS, and a Cloudflare account on the free tier for geo data and CDN.

The first three commands get the code and install dependencies:

```bash
git clone https://github.com/mddanishyusuf/traffic-source.git
cd traffic-source
npm install
```

Configuration is copied from the sample file, and four values have to be set:

```bash
cp .env.local .env.production
```

```bash
JWT_SECRET=your-random-64-char-hex-string
JWT_EXPIRY=7d
NEXT_PUBLIC_APP_URL=https://your-domain.com
DATABASE_PATH=./data/analytics.db
```

The JWT secret is generated with a local openssl call rather than invented:

```bash
openssl rand -hex 32
```

Then the app is built and started, and it listens on port 3000:

```bash
npm run build
npm start
```

Note that the repository ships a yarn.lock while the documented install path uses npm install. Either works, but mixing the two package managers across machines is a cheap way to get inconsistent dependency trees.

## Cloudflare is not optional if you want city level geo data

Geo tracking does not come from a geolocation database. The README states that country and city level visitor data arrives via Cloudflare proxy headers, and the setup section spells out the sequence: add the domain to Cloudflare on the free plan, point the DNS A record at the VPS IP, and enable the orange cloud proxy toggle so Cloudflare sends the cf-ipcountry and cf-ipcity headers.

That makes Cloudflare a dependency of the geo feature rather than an optional CDN in front of it. Without the proxy enabled, the dashboard still records traffic but the location data has no source.

Behind that sits the standard Nginx reverse proxy block that forwards the request to port 3000 and passes the client address through:

```nginx
server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

One line in that block deserves a second look before you copy it. The X-Forwarded-For header is set from $proxy_forwarded_for, which is not the standard Nginx variable for the client address chain, so the client IP that reaches the app may not be what you expect. Since the dashboard reports referrers and geo, verify what the app actually records before trusting the numbers. The README also notes that Cloudflare handles SSL, so you can use Cloudflare's Origin CA certificate or Full (Strict) mode.

## A three kilobyte snippet that follows single page app navigation

Installing the tracker is one script tag in the head of your site, after you create the site in the dashboard and get its identifier:

```html
<script defer src="https://your-domain.com/t.js" data-site="YOUR_SITE_ID"></script>
```

The README describes it as a lightweight script of roughly 3KB with SPA support through pushState and popstate, so a client-side route change is recorded as a pageview rather than being invisible to the tracker. The script also captures referrers and UTM parameters, screen dimensions, and affiliate referrals, which arrive through a ?ref=affiliate-slug query parameter on the incoming link.

That query parameter is the hinge of the affiliate system. A dashboard entry creates affiliates with a custom commission rate, hands out a shareable referral link, and exposes public dashboards for them. The tracker picks the slug off the URL and attributes the visit to that affiliate, which is how the later commission figure gets attached to a session.

The snippet is served from your own domain, so no third party sees the traffic. The trade-off is that you are responsible for serving t.js reliably, and any ad blocker rule that targets analytics hosts can still block it, since blocking is a function of the rule, not the hosting location.

## Stripe conversions by polling, with visitor ids in checkout metadata

Conversion tracking is where this project differs from a plain analytics dashboard, and the mechanism is deliberately simple. You add a Stripe Secret Key in the site settings, and when you create a Checkout Session you pass two identifiers in the metadata: the visitor id and the session id that the tracking script exposes.

```js
const session = await stripe.checkout.sessions.create({
  // ...your checkout config
  metadata: {
    ts_visitor_id: window.__ts.vid,
    ts_session_id: window.__ts.sid(),
  },
});
```

Traffic Source then polls Stripe every 60 seconds and matches payments to visitor sessions on its own, so there is no webhook endpoint to expose and no signature to verify. The visitor journeys view can then show every page a visitor viewed before converting.

The cost of that simplicity is latency. A conversion appears up to a minute after the payment, and the matching depends on your own checkout code writing the metadata correctly. If you create Checkout Sessions anywhere other than the path you edit, those payments will not be attributed, and the README does not describe a way to reconcile them later.

## A shared OAuth client and an encryption key you have to back up

The Google Search Console integration keeps the last 90 days of keyword data and exposes winners, losers, opportunities, quick wins and a keyword explorer that shows pages, countries and devices for a single query. The setup is a one-time job per instance: create a Google Cloud project, enable the Search Console API, configure the OAuth consent screen as External with the .../auth/webmasters.readonly scope, create a Web application OAuth client, and paste the Client ID and Client Secret into Settings, then Integrations.

Two constraints follow from that design. The README states that anyone using the instance shares the same OAuth client, so there is one authorisation for the whole installation rather than per-user Google accounts. And the redirect URI has to match the URI shown under Settings, Integrations, which is auto-detected from the deployed domain, so changing domains means revisiting the Google Cloud credentials.

Credentials and refresh tokens are encrypted at rest with AES-256-GCM. The encryption key is generated on first use and stored in data/.appkey, and the README explicitly says to back it up alongside the database. Lose that file and the stored credentials cannot be decrypted, and since data/.appkey and the database both sit inside the project directory, your backup of the SQLite file alone is not a backup of the installation.

## No licence file, no releases, and a private package manifest

The repository's top level contains .gitignore, .npmrc, README.md, demo-image.png, eslint.config.mjs, instrumentation.js, jsconfig.json, next.config.mjs, package.json, public/, scripts/, src/ and yarn.lock. There is no LICENSE file among them, and no licence identifier is stated for the project, which is a genuine gap for anyone planning to run it as part of a business.

The package.json is marked private at version 0.1.0, with scripts for dev, build, start, lint and deploy, where deploy runs bash scripts/deploy.sh. That script is described as pulling the latest changes, building in a temp directory, swapping atomically and restarting PM2, and it is started with a single command:

```bash
npm run deploy
```

PM2 is the recommended process manager, with a global install and a named process:

```bash
# Install PM2 globally
npm install -g pm2

# Start the app
pm2 start npm --name "trafficsource" -- start

# Auto-restart on reboot
pm2 startup
pm2 save
```

The last push was on 2026-04-13 and the repository has no GitHub releases, so upgrades arrive as commits on main rather than as tagged versions. There is no documented upgrade or rollback procedure, and the atomic swap in the deploy script is the only protection you get against a broken build.

## Conclusion

Adopt traffic-source if you want pageview, referrer and Stripe conversion data on infrastructure you control, and you are willing to run Node, a Cloudflare proxy and a SQLite file yourself. Skip it if you need a hosted product with a support contract, multi-tenant isolation, or a licence you can point to, because the repository carries no LICENSE file and its package.json is marked private. Before deploying, check the Nginx snippet in the README, since the X-Forwarded-For line uses a variable name that differs from the standard one, and confirm that the first registered account becomes the only account allowed to register. The last push was on 2026-04-13 and there are no GitHub releases.

## FAQ

### Does traffic-source use cookies to track visitors?

No. The README lists no cookies for tracking as a feature, and the tracking script captures pageviews, referrers, UTM parameters and screen dimensions directly. All the data is written to your own SQLite database.

### How does traffic-source track Stripe payments?

It polls Stripe every 60 seconds and matches payments to visitor sessions on its own, so no webhook setup is needed. The visitor and session identifiers have to be passed in the Checkout Session metadata, which means your own checkout code has to be edited for attribution to work.

### Why do I need Cloudflare to get geo data in traffic-source?

Country and city level data comes from Cloudflare proxy headers such as cf-ipcountry and cf-ipcity, so your domain has to be on Cloudflare with the proxy toggle enabled. Without that, the dashboard records traffic but has no source for location.

### Can more than one user register on a traffic-source instance?

No. The README states that only the first user can register, and that registration is disabled after that. Plan the first account carefully, since later accounts cannot be added through the interface.

## Sources

- [Issues](https://github.com/mddanishyusuf/traffic-source/issues)
- [mddanishyusuf/traffic-source on GitHub](https://github.com/mddanishyusuf/traffic-source)
- [Project website](https://piqo.app)
- [README](https://github.com/mddanishyusuf/traffic-source/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mddanishyusuf-traffic-source
