Quick start
You need Node.js 18 or newer. Create a folder for your instance and start the server:
mkdir my-analytics && cd my-analytics
npx tacilytics serve # http://127.0.0.1:3920The database (data/analytics.db) and your settings (.env) live in that folder, so updating the package never touches your data. Prefer a global install? npm i -g tacilytics, then tacilytics serve.
From source:
git clone https://github.com/Open-Cosmic-Software/tacilytics.git
cd tacilytics && npm install
cp .env.example .env # set your own secrets
npm startWEB_PASSWORD the dashboard uses the default passphrase tacilytics, and the server prints a warning on start.Add the script
Open the dashboard, add your site, then put this line in the <head> of every page:
<script defer data-domain="example.com"
src="https://analytics.example.com/tacilytics.js"></script>Page changes in single-page apps (pushState, popstate) are tracked automatically. Bots and crawlers are dropped on the server.
Configuration
Set these in .env next to your data, or as environment variables.
| Variable | Default | Purpose |
|---|---|---|
PORT | 3920 | Port, bound to 127.0.0.1 |
WEB_PASSWORD | tacilytics | Dashboard passphrase. Always set your own. |
SESSION_SECRET | random | Secret for login sessions. Set it so logins survive restarts. |
INGEST_SALT | fixed | Extra server secret mixed into the daily visitor hash. |
API_KEY | none | Bearer key for the REST API and the CLI. |
DB_PATH | ./data/analytics.db | Location of the SQLite database. |
DATA_RETENTION_DAYS | 0 | 0 keeps raw events forever; any other number deletes raw events older than that, daily. |
TLS & reverse proxy
The server only listens on 127.0.0.1. Put a TLS proxy in front, for example Caddy, which fetches certificates automatically:
analytics.example.com {
encode gzip
reverse_proxy 127.0.0.1:3920
}Events & goals
Record any action as a named event, optionally with properties:
tacilytics('Signup');
tacilytics('Upgrade', { plan: 'pro' });Events appear under Goals & conversions with a conversion rate. Click one to filter the whole dashboard to the visitors who triggered it. Properties are limited to short scalar values, so they can't turn into a store of personal data.
Purchases & revenue
Send a Purchase event with a value and currency to see revenue, average order value and revenue by source and platform:
tacilytics('Purchase', { value: '49.90', currency: 'EUR' });Visitors returning from payment providers (PayPal, Klarna and the like) are recognised and not counted as a new traffic source.
Script options
<script defer data-domain="example.com"
data-honor-dnt
data-track-outbound
src="https://analytics.example.com/tacilytics.js"></script>| Attribute | Effect |
|---|---|
data-honor-dnt | Sends nothing when Do Not Track or Global Privacy Control is switched on. |
data-track-outbound | Records clicks on links to other domains as events. |
Mobile & desktop apps
Apps report into the same site as your website. Every event carries a platform (web, android, ios, macos, windows, linux), and the dashboard gains a platform filter, app versions and revenue per platform.
Flutter
The package lives in sdk/flutter:
Tacilytics.init(endpoint: 'https://analytics.example.com', domain: 'example.com', appVersion: '1.4.0');
Tacilytics.instance.setEnabled(userConsented);
MaterialApp(navigatorObservers: [TacilyticsRouteObserver()]);
Tacilytics.instance.search('linen shirt', 6);
Tacilytics.instance.purchase(value: 49.90, currency: 'EUR', items: 2);Any other stack
Send one JSON POST /api/collect per screen view or event:
| Field | Meaning |
|---|---|
d | Site domain, as registered |
p | Screen, written like a path, e.g. /product/42 |
pf | Platform: android, ios, macos, windows, linux |
sid | Random ID drawn per app launch and kept in memory only (16 to 64 characters) |
av / dv | App version / device class (mobile, tablet, desktop), optional |
l / cc | Language / two-letter region of the device locale, optional |
n / pr | Event name and flat properties, optional |
No advertising ID, no device ID, no cookies. The launch ID is hashed with the daily salt, so a user is never followed across days.
REST API
Everything the dashboard shows is available with a Bearer key (API_KEY):
curl -H "Authorization: Bearer $TACILYTICS_API_KEY" \
"https://analytics.example.com/api/v1/stats?site=example.com&period=30d&country=DE"| Endpoint | Description |
|---|---|
GET /api/v1/sites | List tracked sites |
POST /api/v1/sites | Add a site { "domain": "…" } |
GET /api/v1/stats | Aggregated stats: period = today, 7d, 30d, 90d, 12mo, or from/to |
GET /api/v1/realtime | Visitors online, 30-minute pulse and recent events |
Add country, source, page, browser, os, device, lang, medium, campaign or goal to /stats to segment.
CLI
export TACILYTICS_URL=https://analytics.example.com
export TACILYTICS_API_KEY=your-key
tacilytics sites
tacilytics stats example.com 30d
tacilytics live example.com --watch
tacilytics add example.comAdd --json to any command for machine-readable output.
How counting works
The script sends a small beacon with the page, referrer, timezone and language. The server reads browser, operating system and device class from the user agent, drops bots, maps the timezone to a country and builds the daily visitor hash. One row is stored per pageview; the dashboard aggregates with plain SQL.
The honest trade-off: without a persistent identifier, Tacilytics counts unique visitors per day and can't follow one person across days or devices. That is the point.
Privacy & security
- No cookies, no local storage, IP addresses are never written to disk.
- The daily salt lives in its own file (
data/.day-salt, mode 600), never in the database, and is overwritten in place when the day changes. Events that arrive late get a one-off hash that can't be linked. - Copy-on-write filesystems, snapshots and backups can keep an old salt physically. Treat backups of
data/like the live server, or exclude.day-salt. - Run a single process per data folder.
- Login and the collect endpoint are rate-limited; everything visitors send is escaped before it reaches the page.