liwan - Lightweight Analytics https://liwan.dev
  • Rust 54.6%
  • TypeScript 35.1%
  • CSS 6.3%
  • Astro 3.8%
  • Shell 0.2%
Find a file
2026-06-17 14:29:27 +01:00
.cargo chore: cleanup, add dev command 2026-06-03 22:00:56 +02:00
.github ci: build the container image from the fork's own workflow and releases 2026-06-05 08:42:22 +01:00
benches feat: timezone-aware graph ranges 2026-05-03 15:25:28 +02:00
data feat: import historical pageviews from Matomo 2026-06-17 14:29:27 +01:00
scripts chore: prepare v1.5.0 release 2026-05-10 13:47:31 +02:00
src feat: import historical pageviews from Matomo 2026-06-17 14:29:27 +01:00
tests feat: entity dimension and per-entity report scope selector 2026-06-09 17:45:41 +01:00
tracker chore: prepare v1.5.0 release 2026-05-10 13:47:31 +02:00
web feat: entity dimension and per-entity report scope selector 2026-06-09 17:45:41 +01:00
.gitignore feat: entity dimension and per-entity report scope selector 2026-06-09 17:45:41 +01:00
.rustfmt.toml chore: run biome, cleanup repo 2024-11-24 14:20:41 +01:00
about.toml chore: upgrade deps, switch to base-ui (#33) 2026-03-09 13:31:28 +01:00
audit.toml ci: add audit.toml 2024-09-11 14:37:50 +02:00
biome.json chore: general cleanup 2026-06-03 22:00:56 +02:00
Cargo.lock feat: import historical pageviews from Matomo 2026-06-17 14:29:27 +01:00
Cargo.toml feat: import historical pageviews from Matomo 2026-06-17 14:29:27 +01:00
CHANGELOG.md feat: import historical pageviews from Matomo 2026-06-17 14:29:27 +01:00
LICENSE.md chore: relicense under apache-2.0 2025-03-27 23:37:30 +01:00
NOTICE.md chore: prepare v1.5.0 release 2026-05-10 13:47:31 +02:00
README.md feat: import historical pageviews from Matomo 2026-06-17 14:29:27 +01:00


liwan.dev - Easy & Privacy-First Web Analytics

GitHub Workflow Status GitHub Release Container

   

Features

  • Quick setup
    Quickly get started with Liwan with a single, self-contained binary . No database or complex setup required. The tracking script is a single line of code that works with any website and less than 1KB in size.
  • Privacy first
    Liwan respects your users privacy by default. No cookies, no cross-site tracking, no persistent identifiers. All data is stored on your server.
  • Lightweight
    You can run Liwan on a cheap VPS, your old mac mini, or even a Raspberry Pi. Written in Rust and using tokio for async I/O, Liwan is fast and efficient.
  • Open source
    Fully open source. You can change, extend, and contribute to the codebase.
  • Accurate data
    Get accurate data about your websites visitors, page views, referrers, and more. Liwan detects bots and crawlers and filters them out by default.
  • Real-time analytics
    See your websites traffic in real-time. Liwan updates the dashboard automatically as new visitors come in.

OIDC/SSO

This fork focuses on OIDC/SSO login. For everything else - sites, users, roles, tracking, and the rest - see the docs at liwan.dev/getting-started.

Sign in with a username and password, or connect your identity provider over OIDC/SSO. With OIDC enabled, a "Sign in with SSO" button appears on the login page and accounts are provisioned automatically on first login. The username is taken from the provider's preferred_username, then a verified email, and — when neither is available — the user's name (e.g. Jane Doejane-doe), falling back to the opaque subject ID only as a last resort; either way, the account is matched on the provider's subject, so email or name changes never break the link. The first admin is created through a one-time setup flow, and accounts can also be managed from the CLI.

Configuration

Liwan reads a single TOML file. It looks for ./liwan.config.toml, then $XDG_CONFIG_HOME/liwan/config.toml (i.e. ~/.config/liwan/config.toml), or you can point it at an explicit path with --config <path> or the LIWAN_CONFIG env var. Any value can also be overridden with a LIWAN_* environment variable (e.g. LIWAN_OIDC_CLIENT_SECRET), which is the recommended way to pass secrets. A fully annotated example lives in data/config.example.toml.

base_url = "https://analytics.example.com"  # external URL of this instance (used to build the OIDC redirect URI)
listen   = 9042                             # local http port to bind, typically behind a reverse proxy
# data_dir = "./liwan-data"                 # defaults to ~/.local/share/liwan/data

OpenID Connect (OIDC)

Add an [oidc] section to enable SSO. It only activates once issuer, client_id, and client_secret are all set — at which point a "Sign in with SSO" button appears on the login page. Password login keeps working alongside it.

[oidc]
issuer        = "https://idp.example.com"        # discovery base; /.well-known/openid-configuration is appended
client_id     = "liwan"
client_secret = "..."                            # prefer the LIWAN_OIDC_CLIENT_SECRET env var
scopes        = ["openid", "email", "profile"]   # optional; this is the default
button_label  = "Sign in with SSO"               # optional; label on the login button

Register this redirect URI with your provider (derived from base_url):

<base_url>/api/dashboard/auth/oidc/callback

First-time SSO users are created as regular users with no project access until an admin grants it; usernames are derived from the OIDC claims as described above. Accounts are matched on the provider's subject, so a later email or name change never breaks the link.

Restricting who can register

By default any user your IdP authenticates gets a local account on first login. Two options narrow that — they gate first-time provisioning only, so returning users (matched on the provider's subject) always log in regardless:

[oidc]
# ... issuer, client_id, client_secret as above ...

# open (default) | closed | domain_allowlist
registration    = "domain_allowlist"
allowed_domains = ["example.com", "acme.org"]    # required for domain_allowlist
  • closed — no new accounts; only users who already exist can log in. Handy once everyone intended is enrolled.
  • domain_allowlist — new accounts only for users whose verified email is in allowed_domains. An unverified email never qualifies, so the email scope is required. Matching is exact and case-insensitive — subdomains do not match (example.com excludes sub.example.com). The trust here assumes your single configured issuer is authoritative for those domains. Configure non-ASCII (IDN) domains in punycode.

Rejections send the user back to the login page with a short explanation. Both fields also accept env overrides (LIWAN_OIDC_REGISTRATION, LIWAN_OIDC_ALLOWED_DOMAINS as a comma-separated list).

Importing from Matomo

Liwan can import historical pageviews from a Matomo instance. The importer is opt-in at build time — default builds don't include it:

cargo build --release --features import-matomo

You need Matomo 5.0 or newer (the importer checks before doing anything else), an API token, and the liwan server must be stopped — DuckDB allows a single writer, the same limitation prune has.

MATOMO_TOKEN=... liwan import matomo \
  --url https://matomo.example.com \
  --site 3=blog --site 7=docs \
  --since 2022-01-01
  • --site <idSite>=<entity_id> maps a Matomo site to a liwan entity. Repeatable; one entity per site, and the entity must already exist.
  • --since YYYY-MM-DD is required on the first run per site.
  • The token can also be passed with --token, but prefer MATOMO_TOKEN — CLI arguments leak via ps and shell history.
  • --page-size <n> sets visits per API request (default 1000); --dry-run fetches and maps without writing anything.
  • --drop-local-urls skips pageviews whose URL host is localhost or a private/reserved IP (e.g. 127.0.0.1) — useful for filtering local development traffic that was tracked into Matomo. Off by default, to match live tracking (which keeps these).

Re-runs are incremental. The importer keeps a per-site checkpoint file under <data_dir>/import/ and only fetches what's newer, in month-sized chunks. There's a one-hour lateness allowance: actions that arrive in Matomo more than about an hour after the fact (QueuedTracking, log importer) fall below the watermark and won't be picked up by later runs. To re-import from scratch, delete the checkpoint file and run again with --since — that's safe, since resuming first deletes previously imported rows newer than the watermark.

The importer refuses to run when the target entity has day-based data retention or a drop rule that would discard every imported event — the next prune would just delete the history again. --force overrides this if you know what you're doing.

Do expect the numbers to differ from Matomo's own, and from live liwan tracking:

  • Cookieless Matomo sites count uniques per day, so liwan shows more unique visitors over multi-day ranges. Imported visitors are also distinct from live visitors — someone tracked by both appears twice.
  • Only pageviews are imported; downloads, events, and goals between pageviews are dropped, which skews bounce rate and time-on-site (bounce rate generally higher).
  • Time-on-site isn't comparable to Matomo's: liwan averages the gap between consecutive pageviews (single-page sessions excluded), while Matomo divides total visit length by all visits (bounces counted as zero). On bounce-heavy sites liwan's figure reads much higher. This is how liwan computes the metric for live data too, not an import artifact.
  • Bot filtering is Matomo's, not liwan's. Device and OS mapping is best-effort. City names can differ between GeoIP databases.

Fork

This is a fork of explodingcamera/liwan, adding support for an OIDC/OAuth login flow.

License

Unless otherwise noted, the code in this repository is available under the terms of the Apache-2.0 license. See LICENSE for more information.