@almadar/analytics-server
v0.1.0
Published
Tiny cookieless analytics collector: sendBeacon ingest, daily-salt uniqueness, server-side GeoIP then IP discarded.
Maintainers
Readme
@almadar/analytics-server
A tiny cookieless analytics collector for @almadar/analytics.
Zero required dependencies — plain node:http + node:crypto, an append-only
NDJSON store, and optional MaxMind GeoIP. Mountable as a standalone server or
into any Node HTTP framework.
How it stays GDPR-friendly
- No cookies, no identifiers leave the browser. The client sends only the event; the collector reads the request IP and User-Agent server-side.
- Daily-rotating salt. Visitor uniqueness is
sha256(dailySalt + ip + ua + domain). The salt lives in memory and rotates every UTC day, so a visitor cannot be linked across days, and the hash is never reversible to an IP. - IP is discarded. It is used only to derive the daily hash and (optionally) country/region, then it drops out of scope. It is never stored, logged, or returned.
- Geography is coarse. Country + region (ISO codes) only, from GeoLite2.
Run it
npm i -g @almadar/analytics-server # or run from the monorepo
almadar-analytics-server # listens on :8787Configuration is via environment variables:
| Var | Default | Description |
|---|---|---|
| PORT | 8787 | Listen port. |
| HOST | all | Bind host. |
| ANALYTICS_DATA | ./analytics.ndjson | NDJSON store path. |
| ANALYTICS_GEOIP_DB | (off) | Path to a GeoLite2 .mmdb; enables geography. |
| ANALYTICS_TRUST_PROXY | true | Read client IP from X-Forwarded-For. |
| ANALYTICS_ALLOW_ORIGIN | * | Access-Control-Allow-Origin. |
| ANALYTICS_STATS_TOKEN | (off) | Bearer token for GET /stats; unset = disabled. |
Geography needs a MaxMind GeoLite2 database (not redistributable): sign up for a
free MaxMind account, download GeoLite2-City.mmdb, point ANALYTICS_GEOIP_DB
at it, and npm i maxmind (it is an optional dependency). Behind a CDN/proxy,
forward the real visitor IP in X-Forwarded-For or geography and uniqueness
break.
Endpoints
POST /e— ingest (202). Body is the beacon JSON.GET /health—200 ok.GET /stats?domain=&from=&to=&limit=— JSON aggregates. RequiresAuthorization: Bearer <ANALYTICS_STATS_TOKEN>; disabled when no token is set.
Embed in an existing server
import { createCollector, FileStore, createGeo } from '@almadar/analytics-server';
const collector = createCollector({
store: new FileStore('./analytics.ndjson'),
geo: await createGeo(process.env.ANALYTICS_GEOIP_DB),
statsToken: process.env.ANALYTICS_STATS_TOKEN,
});
// node:http
http.createServer((req, res) => collector.handle(req, res));Storage
The bundled FileStore (NDJSON, loaded into memory on startup) is fine for low
-to-moderate volume. For high write volume, implement the Store interface
(record / stats / prune) over SQLite or Postgres and pass it instead — the
collector depends only on that interface. Honor data-minimization retention
(CNIL caps audience-measurement data at ~25 months): run store.prune(cutoff)
on a schedule.
