npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tadoru

v0.6.3

Published

Cookieless, privacy-first, self-hosted web analytics. One process, one file, no consent banner.

Readme

Tadoru (辿る)

Self-hosted web analytics without cookies, without a consent banner, and without sending anything to anyone else. One Node process and one SQLite file on your own server.

Tadoru is Japanese for following a trail — which is what analytics should do to pages, not to people.


Why no consent banner

Not because it avoids cookies. Removing cookies is not enough on its own: EDPB Guidelines 2/2023 extended ePrivacy Art. 5(3) to tracking pixels, tracking links, fingerprinting and certain IP-based tracking. What actually removes the banner is the audience-measurement exemption, and Tadoru is built to satisfy it by construction rather than by configuration.

  • Visitors are counted with a hash of a salt that is destroyed every 24 hours, so nothing links one day to the next.
  • The site's domain is part of that hash, so the same person on two sites is two unrelated visitors. Cross-site tracking is not disabled; it is impossible.
  • IP addresses and user-agents are never written to disk. They are used in memory to derive the hash and to look up a country, then discarded.
  • No fingerprinting. No canvas, no WebGL, no font list, no exact timezone, no exact resolution, no plugin list. The tracker payload is a closed allow-list guarded by a test.
  • Do Not Track and Global Privacy Control are honoured before any request is sent.
  • Raw events expire after 25 months by default.

The full reasoning, condition by condition, is in docs/compliance.md. A paste-ready privacy notice is in docs/privacy-policy-template.md.

You remain the data controller for your installation. This is engineering documentation, not legal advice.


Try it on your own computer first

Before touching a server, run it locally. Nothing is installed system-wide and you can delete it afterwards by deleting one folder. You need Node.js 22 or newer — check with node -v.

npm install -g tadoru

TADORU_ADMIN_PASSWORD=test1234 \
TADORU_SITES=example.com \
TADORU_DATA_DIR=./tadoru-data \
  tadoru start

Open http://127.0.0.1:3000 and log in with test1234. The dashboard will be empty, which is correct — nothing has visited yet.

If something else on your machine already has port 3000 — a development server, usually — Tadoru notices and tries 3001, 3002, and so on up to 3009 until it finds a free one. The startup line printed in your terminal always names the port it actually bound, e.g. listening on 127.0.0.1:3001 (3000 was busy; set TADORU_PORT to pin it) — use that number below instead of 3000 if it differs.

To see it record something, leave that running and in another terminal:

curl -H 'Referer: https://example.com/hello' http://127.0.0.1:3000/t.gif

Reload the dashboard and there is your first visit. Press Ctrl+C in the first terminal to stop, then rm -rf ./tadoru-data to remove everything it wrote.


Install on a server

If you already run servers, this is the whole thing:

sudo npm install -g tadoru
sudo tadoru install-service --sites example.com,other.dev
sudo systemctl enable --now tadoru

install-service creates the tadoru system user, writes /etc/tadoru/tadoru.env with a freshly generated admin password (printed once — write it down), writes the systemd unit, and reloads systemd. It never starts or enables the service itself: that stays a separate, explicit step, which is what systemctl enable --now above is. Re-running install-service is safe — it never overwrites an existing tadoru.env, so your admin password never changes underneath you.

Then a reverse proxy for HTTPS, and the snippet on your site. Both are in the walkthrough below.


Step by step, assuming nothing

This section explains every step and every word. Skip it if the three commands above were enough.

What you need before starting

A server that stays on. A cheap VPS is fine — Tadoru uses about 100 MB of memory and one small file for storage, so the smallest plan any provider sells will do. Hetzner, DigitalOcean, OVH and Scaleway all rent one for a few euros a month. When you create it, choose Ubuntu or Debian if you are unsure; the commands below assume one of those.

A domain name. You will point a subdomain like stats.yoursite.com at the server, so the dashboard has an address. If your site already has a domain, you can use a subdomain of it and pay nothing extra.

A way to connect to the server. Your provider gives you an IP address and a password or key. You connect from your own terminal with ssh root@THE-IP-ADDRESS. Everything below is typed in that connection, not on your own computer.

1. Install Node.js

Tadoru is a Node.js program, so the server needs Node 22 or newer. Check what is there:

node -v

If that prints nothing, or a number below v22, install it:

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

Run node -v again. You want to see v22 or higher.

Node 20 and older are not supported. Node 20 stopped receiving security updates in April 2026, and running a public service on an unpatched runtime is a bad trade.

2. Install Tadoru

sudo npm install -g tadoru

npm is Node's package installer. -g means "install it for the whole machine", so the tadoru command works anywhere. Check it landed:

tadoru --version

3. Set it up

Replace example.com with the domain of the site you want to measure. You can list several, separated by commas and no spaces.

sudo tadoru install-service --sites example.com

This prints a list of what it did, ending with a password. Copy that password somewhere safe now — it is shown once and never again. It is how you log into the dashboard.

If you want to see what it would do without doing it, add --dry-run first.

4. Start it

sudo systemctl enable --now tadoru

systemctl manages background programs on Linux. enable means "start this automatically whenever the server reboots" and --now means "and also start it right now". Check it is running:

sudo systemctl status tadoru

You want a line saying active (running). Press q to get out of that screen.

5. Point your domain at the server

In whoever manages your domain — your registrar or DNS provider — add an A record:

| Field | Value | |---|---| | Type | A | | Name | stats | | Value | your server's IP address |

An A record is simply the line that tells the internet "this name lives at this IP address". After adding it, stats.yoursite.com points at your server. It usually works within minutes, though it can take longer.

6. Give it a way in from the internet

Tadoru listens only on the server's own internal address — 127.0.0.1:3000 — so nothing outside can reach it yet. That is deliberate: it should never face the internet directly.

What sits in front is your choice, and this guide is not going to pick for you. nginx, Caddy, Apache, Traefik, HAProxy, a load balancer your host provides — any of them works, and on a server you already use, one of them is probably running and holding ports 80 and 443 already. Installing a second one will simply fail to start.

So instead of a recipe, here is the contract. Point a subdomain at your server, terminate HTTPS there, and forward to 127.0.0.1:3000 passing four things:

| What to pass | Why it matters if you do not | |---|---| | The request itself, to 127.0.0.1:3000 | Nothing works at all. | | Host: the public hostname | The dashboard builds its ready-to-paste snippet from this. Get it wrong and it hands you a snippet pointing at 127.0.0.1. | | X-Forwarded-Proto: https | The admin session cookie loses its Secure flag, so it can travel over plain HTTP. | | X-Forwarded-For: the real client IP | Every visitor resolves to your server's own country, and the country breakdown becomes meaningless. |

One detail worth getting right, because most tutorials get it wrong. Tadoru reads the first entry of X-Forwarded-For. Your proxy must replace that header with the connecting IP, not append to it. If it appends — nginx's widely copied $proxy_add_x_forwarded_for does exactly this — then a visitor who sends their own X-Forwarded-For ends up first in the list, and can choose which country they appear from. Since your proxy is the one facing the internet, the IP it sees is the true one; replace with that.

Two examples, not recommendations. nginx:

server {
    listen 80;
    server_name stats.yoursite.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Then add HTTPS however you normally do — certbot --nginx -d stats.yoursite.com is the usual way.

Caddy, which sets all three headers itself and obtains the certificate unprompted:

stats.yoursite.com {
	reverse_proxy 127.0.0.1:3000
}

TADORU_TRUSTED_PROXY=true is already set in /etc/tadoru/tadoru.env, which is what allows those forwarded headers to be believed. Leave it on only while something really is in front: with nothing there, anyone could send the headers themselves.

Now open https://stats.yoursite.com in a browser. You should see the login page. Enter the password from step 3.

7. Add the snippet to your site

In the HTML of the site you are measuring, just before </head>:

<script defer src="https://stats.yoursite.com/t.js"></script>

For visitors with JavaScript turned off, add this too:

<noscript><img src="https://stats.yoursite.com/t.gif" alt="" width="1" height="1"></noscript>

8. Check that it works

This is the step people skip and then wonder whether they finished.

  1. Open your site in a browser and click through a couple of pages.
  2. Go back to https://stats.yoursite.com and reload.
  3. You should see those visits.

If you see them, you are done.


When it does not work

The dashboard says nothing was recorded. Check that the domain in --sites is exactly the one your visitors use. Tadoru silently ignores traffic from any domain not on that list, including www. differences.

systemctl status tadoru says failed. Read the reason:

sudo journalctl -u tadoru -n 30

The most common cause is a missing admin password — Tadoru refuses to start rather than run with no credentials, and says so plainly.

The browser cannot reach the address. The DNS record has probably not taken effect yet. Check what the world sees:

dig +short stats.yoursite.com

It should print your server's IP. If it prints nothing, wait and try again.

HTTPS shows a certificate warning. Whatever issues your certificate could not get one — almost always because the DNS record is not pointing at this server yet, or because ports 80 and 443 are blocked by a firewall. Your proxy's own log says which: sudo journalctl -u nginx -n 30, or caddy, or whatever you are running.

Your proxy refuses to start with address already in use. Something else already holds port 80 or 443 — on a server you already use, that is normal. Find it with sudo ss -tlnp | grep -E ':80|:443' and add Tadoru to that server rather than installing a second one.

You lost the admin password. /etc/tadoru/tadoru.env stores only a scrypt hash and its salt (TADORU_ADMIN_PASSWORD_HASH / TADORU_ADMIN_PASSWORD_SALT), never the plaintext. A hash cannot be turned back into the password, so losing it is genuinely unrecoverable rather than merely inconvenient — reset it instead:

sudo tadoru reset-password
sudo systemctl restart tadoru

This backs up the existing env file to a dated copy alongside it, generates a new password, prints it once, and rewrites only the credential lines — every other line in the file (comments, sites, host, port) is left untouched.


Measuring more than page views

The snippet already records pages, referrers, campaigns, countries, devices, engagement time, scroll depth and outbound clicks without any configuration. Two things you can add.

Goals and custom events. Call tadoru with a name wherever something happens that matters — a signup, a purchase, a form sent:

tadoru('signup', { plan: 'pro' })

The name appears in the dashboard; the properties are stored alongside it.

Visitors with JavaScript disabled. The <noscript> pixel from step 7 covers them. It records the page, referrer, country and device, but not engagement, scroll or clicks — those need a script running in the page.


Operating it

systemctl status tadoru      # is it running
journalctl -u tadoru -f      # what is it doing
curl localhost:3000/health   # database and scheduled jobs
tadoru status                # one command answering "is Tadoru working?"
tadoru backup                # a consistent copy of the whole database
sudo npm update -g tadoru && sudo systemctl restart tadoru

One command, one answer. tadoru status checks whether a server is answering, opens the database read-only to report its size and per-site event counts, and shows each scheduled job's health — without you having to chain systemctl, curl and sqlite3 by hand. It also contacts the npm registry to check whether a newer version has been published, and if so reports it by name alongside the upgrade command; --no-update-check skips that request entirely. It exits non-zero if the server isn't answering or the database can't be read, so it doubles as a monitoring check. Add --json for scripting:

tadoru status --json | jq .server

Salt rotation, nightly rollups and retention purging run inside the process. There is no crontab to configure.

Backups are one file. tadoru backup writes a dated, consistent copy; restoring is copying it back. That simplicity is the direct payoff of choosing SQLite.

Restoring is a command, not "copy the file back". tadoru restore <backup-file> does the copy for you, at the moment an operator is most likely to destroy the data they meant to save:

tadoru restore /var/lib/tadoru/tadoru-backup-2026-09-08T12-34-56.sqlite

It refuses to run while the server is answering — restoring into a database a live process is writing to can corrupt both files, and this is the one guard that isn't recoverable, so --force is required to skip it. It refuses any file that isn't actually a Tadoru database, checking that it opens as SQLite and carries the tables Tadoru expects, so a mistyped path can't silently clobber your installation with an unrelated .db file. Before touching anything it moves the database currently in place to a dated file next to it — so restoring the wrong backup still leaves you a way back — and only then replaces it, removing any stale -wal/-shm files so the restored database never starts from an inconsistent journal. --dry-run runs every one of those checks for real and prints the plan without changing anything on disk.

Lost the admin password? tadoru reset-password. It backs up the env file, generates a new password, prints it once, and rewrites only the credential lines — see "When it does not work" above for the full walkthrough.

Keeping country lookups accurate. sudo tadoru update-geoip refreshes the bundled GeoIP database. Countries are resolved locally, against a copy that ships inside the package, so nothing is ever asked of an outside service — the cost of that is a copy that slowly ages. Restart afterwards.

tadoru init is not part of this. It writes a tadoru.config.json and a tadoru.env in the current directory, for running tadoru start yourself. A systemd installation reads neither: it is configured entirely from /etc/tadoru/tadoru.env, which install-service writes. Running init before install-service is harmless but pointless, and the password it prints is not the one that opens the dashboard.

A note on hardening. Running natively means there is no container isolating the process, so the shipped systemd unit does that work instead: dedicated unprivileged user, no capabilities, ProtectSystem=strict, a restricted syscall filter. Check it with systemd-analyze security tadoru.

Dashboard language. The dashboard is available in English, Spanish and Japanese. It follows your browser's Accept-Language by default; TADORU_LANG (en, es or ja) fixes it to one language instead. install-service always writes that key into the environment file, and the unit loads that file with EnvironmentFile=, which takes precedence over the manager environment — so change it there rather than with systemctl set-environment, which would be silently ignored:

sudo sed -i 's/^TADORU_LANG=.*/TADORU_LANG=es/' /etc/tadoru/tadoru.env
sudo systemctl restart tadoru

Docker, if you prefer it

A single-service docker-compose.yml lives in docker/. There is no database container because there is no database server.


What it collects

| Without JavaScript | Additionally, with the script | |---|---| | Page, referrer, campaign | SPA route changes | | Country | Time actually visible on the page | | Browser, OS and device family | Scroll depth milestones | | Language | Outbound clicks and file downloads | | | Custom events and goals | | | Core Web Vitals (LCP, CLS, INP) | | | Colour scheme, screen size bucket |

Behavioural depth, no identity depth. That trade is the entire design, and docs/adr/ records why each part of it was chosen and what it costs.

Collected is not yet the same as shown. Everything above is recorded and kept, but the dashboard does not have a view for all of it. Today it reports pages, referrers, campaigns, countries, devices, browsers, operating systems, screen size, language, colour scheme and scroll depth. Outbound clicks, custom events and goals, and Core Web Vitals are stored and queryable in the database, but have no section yet — they are recorded from the day you install, so the history is there whenever those views arrive.

What it will never do

Returning visitors, retention curves, cohort analysis and cross-site attribution are all impossible here, by construction. Anyone who needs those needs consent, and therefore a banner, and therefore a different tool.


Development

npm install
npm test                # the full suite
npm run typecheck       # server and tracker, separate configs
npm run dev             # run with --watch
npm run build:tracker   # bundle the browser script, enforcing its size budget

Node 22.18 or newer is required to develop, because the server runs TypeScript directly with no build step. The published package is compiled, so installing it works on any Node 22, which is supported until April 2027.

Start with AGENTS.md — it holds the invariants, the non-obvious toolchain rules and the working method. Behaviour is specified in specs/; every requirement there names the test that enforces it.


Licence

AGPL-3.0-only. You may run it, modify it and redistribute it freely. If you offer a modified version to other people over a network, section 13 requires you to offer them its source too — which is why every dashboard page links back to the repository and the running version.

That is the whole intent: improvements to a privacy tool should stay available to the people whose privacy it protects. See docs/adr/0006-agpl-and-the-network-clause.md.