MIT-licensed reference project · four files

A visitor counter that never writes the raw address.

This small self-hosted service converts the request address into a secret-keyed HMAC in memory, stores only that hash in SQLite, handles browser CORS, excludes common bots and internal checks, and returns aggregate totals—not visitor records.

Download the four-file ZIP ↓

Data path

Request → volatile address → keyed hash → aggregate count.

  1. Accept only POST /track from the configured site origin.
  2. Trust X-Forwarded-For only when the immediate peer is explicitly allowlisted.
  3. Drop empty and common bot user agents plus requests carrying the private internal-check token.
  4. Compute HMAC-SHA256(secret, address) and discard the address variable.
  5. Upsert the hash and expose only unique_visitors and pageviews through GET /stats.
Source files

Small enough to audit before running.

server.mjsNode HTTP server, HMAC hashing, bot exclusion, SQLite upsertPlain-text source
DockerfileUnprivileged Node 22 container with a persistent /data volumePlain-text source
compose.yamlLoopback-only port, secrets, trusted-proxy allowlist, named volumePlain-text source
LICENSEMIT licensePlain-text source
Run it

Set the secrets and proxy address deliberately.

mkdir hmac-counter && cd hmac-counter
# Download the four files above into this directory.
# Edit compose.yaml: set ALLOWED_ORIGIN, two random secrets,
# and the exact address of your trusted reverse proxy.
docker compose up -d --build
curl http://127.0.0.1:8787/health
curl http://127.0.0.1:8787/stats

The compose example binds the service to loopback. Put it behind your existing TLS reverse proxy; do not expose the tracking endpoint directly without rate limiting.

Threat model

Pseudonymous is not anonymous.

A stable HMAC still links repeated requests that share an address. Rotate the secret when you no longer need historical continuity, restrict access to the SQLite volume, document the measurement, and use a shorter retention window if your purpose permits it. Shared NATs also collapse multiple people into one hash; changing addresses may split one person into several.

Verification

What the included checks cover.

  1. Confirm the database schema contains visitor_hash but no raw-address column.
  2. Send the same address twice and expect one unique visitor with two pageviews.
  3. Send a common crawler and the internal-check token and confirm totals do not change.
  4. Send a forged forwarded address from an untrusted peer and confirm it cannot create another visitor.
  5. Send a forwarded address through an allowlisted IPv4-mapped proxy and confirm it is counted.