Mallard Metrics

April 11, 2026 · View on GitHub

Self-hosted, privacy-focused web analytics powered by DuckDB and the behavioral extension. Single binary. Single process. Zero external dependencies.

Tests Rust License Clippy Privacy Docs

A lightweight, privacy-respecting alternative to Plausible Analytics. Runs entirely on your infrastructure — no third-party services, no cookies, no persistent IP storage. See PRIVACY.md for the complete data-processing architecture and operator compliance guidance.


Table of Contents


Features

Privacy by Design

  • No cookies — Visitor identification uses a daily-rotating HMAC-SHA256 hash of IP + User-Agent + daily salt; no cookies are set and no browser storage is accessed
  • No persistent IP storage — IP addresses are processed ephemerally in RAM (for GeoIP lookup and visitor ID derivation) and never written to disk or logs
  • Daily salt rotation — Visitor IDs change every 24 hours; the same IP + User-Agent produces a different hash on different days, preventing cross-day tracking
  • Pseudonymous, not anonymous — Stored visitor IDs are HMAC-SHA256 hashes; pseudonymous data is still personal data under GDPR Recital 26. Geographic data (country, region, city) derived from IP is stored. See PRIVACY.md for the full analysis and operator obligations.

Single Binary Deployment

  • One process handles ingestion, storage, querying, authentication, and the dashboard
  • Zero external dependencies — DuckDB is embedded; no separate database to install or manage
  • FROM scratch Docker image — Static musl binary with no runtime dependencies
  • WAL durability — DuckDB disk-based storage survives crashes without data loss

Analytical Power

  • Core metrics — Unique visitors, pageviews, bounce rate, average session duration
  • Breakdowns — Pages, referrer sources, browsers, operating systems, devices, countries
  • Time-series — Hourly and daily aggregations with chart visualization
  • Funnel analysis — Multi-step conversion funnels via window_funnel()
  • Retention cohorts — Weekly cohort retention grids via retention()
  • Session analytics — Session duration, pages per session via sessionize()
  • Sequence matching — Behavioral pattern detection via sequence_match()
  • Flow analysis — Next-page navigation patterns via sequence_next_node()

GDPR-Friendly Deployment

Mallard Metrics ships a first-class GDPR mode that reduces data collection to the minimum needed for aggregate analytics:

SettingStandardGDPR Mode
Visitor IDHMAC-SHA256 pseudonymous hashHMAC-SHA256 (suppress with suppress_visitor_id)
ReferrerFull URL with query stringPath only — query and fragment stripped
TimestampsMillisecond precisionRounded to nearest hour
Browser infoName + versionName only
OS infoName + versionName only
Screen / deviceStoredOmitted
GeoIPCity-levelCountry-level only

Enable with a single environment variable (MALLARD_GDPR_MODE=true) or configure each flag independently. A DELETE /api/gdpr/erase endpoint supports GDPR Art. 17 right-to-erasure requests. See PRIVACY.md for the full compliance analysis and operator obligations.

Production Ready

  • Argon2id authentication — Password-protected dashboard with cryptographic session tokens
  • API key management — Programmatic access with SHA-256 hashed keys (mm_ prefix, disk-persisted)
  • Rate limiting — Per-site token-bucket rate limiter for ingestion
  • Query caching — TTL-based in-memory cache for analytics queries
  • Bot filtering — Automatic filtering of known bot User-Agents
  • GeoIP resolution — MaxMind GeoLite2 integration with graceful fallback
  • Data retention — Configurable automatic cleanup of old Parquet partitions
  • Graceful shutdown — Buffered events are flushed before process exit
  • Prometheus metricsGET /metrics endpoint with counters for ingestion, cache, auth, and rate limiting
  • Security headers — OWASP-recommended headers including HSTS, CSP, and Permissions-Policy
  • CSRF protection — Origin/Referer validation on all state-mutating endpoints
  • Brute-force protection — Per-IP login lockout with configurable thresholds

Quick Start

docker run -d \
  -p 127.0.0.1:8000:8000 \
  -v mallard-data:/data \
  -e MALLARD_SECRET=your-random-32-char-secret \
  -e MALLARD_ADMIN_PASSWORD=your-dashboard-password \
  ghcr.io/tomtom215/mallard-metrics

Docker Compose

docker compose up -d

The default docker-compose.yml includes persistent storage, restart policy, and environment variable configuration. Set MALLARD_SECRET and MALLARD_ADMIN_PASSWORD in your environment for production.

From Source

# Requires Rust 1.94.0+ (set automatically via rust-toolchain.toml)
cargo build --release
./target/release/mallard-metrics

Visit http://localhost:8000 to access the dashboard. On first visit you will be prompted to set an admin password.


Tracking Script

Add the following snippet to every page you want to track:

<script defer data-domain="yourdomain.com"
  src="https://your-mallard-instance.com/mallard.js"></script>

Custom Events and Revenue Tracking

The tracking script exposes window.mallard() for custom event tracking:

// Track a custom event
window.mallard('signup', {
  props: { plan: 'pro', source: 'landing-page' }
});

// Track revenue
window.mallard('purchase', {
  revenue: 49.99,
  currency: 'USD',
  props: { product: 'annual-plan' },
  callback: () => console.log('tracked')
});
ParameterTypeDescription
propsObjectCustom properties (max 4096 chars)
revenueNumberRevenue amount
currencyStringISO 4217 currency code
callbackFunctionCalled after the event is sent

The tracking script is under 1 KB minified and has zero external dependencies.


Configuration

Mallard Metrics is configured via a TOML file, environment variables, or both. Environment variables override TOML values.

./mallard-metrics /path/to/mallard-metrics.toml

See mallard-metrics.toml.example for a fully documented configuration template.

Key Environment Variables

VariableDefaultDescription
MALLARD_HOST0.0.0.0Server bind address
MALLARD_PORT8000Server listen port
MALLARD_DATA_DIRdataDirectory for Parquet data and DuckDB file
MALLARD_SECRET(auto-generated)HMAC key for visitor ID hashing. Auto-generated and persisted to data_dir/.secret on first run. Set explicitly for production
MALLARD_ADMIN_PASSWORD(none)Admin password for dashboard authentication
MALLARD_SECURE_COOKIESfalseEnable Secure flag on session cookies (required when behind TLS)
MALLARD_METRICS_TOKEN(none)Bearer token protecting the /metrics endpoint
MALLARD_FLUSH_COUNT1000Events buffered before flushing to disk
MALLARD_FLUSH_INTERVAL60Seconds between periodic buffer flushes
MALLARD_GEOIP_DB(none)Path to MaxMind GeoLite2-City.mmdb
MALLARD_DASHBOARD_ORIGIN(none)Restrict dashboard CORS to this origin (enables CSRF protection)
MALLARD_FILTER_BOTStrueFilter known bot User-Agents
MALLARD_RETENTION_DAYS0Auto-delete data older than N days (0 = unlimited)
MALLARD_RATE_LIMIT0Max events/sec per site (0 = unlimited)
MALLARD_CACHE_TTL60Query cache TTL in seconds
MALLARD_CACHE_MAX_ENTRIES10000Maximum cached query results (0 = unlimited)
MALLARD_MAX_CONCURRENT_QUERIES10Maximum concurrent analytics queries (0 = unlimited); excess returns 429
MALLARD_SESSION_TTL86400Dashboard session TTL in seconds (24 hours)
MALLARD_SHUTDOWN_TIMEOUT30Graceful shutdown timeout in seconds
MALLARD_MAX_LOGIN_ATTEMPTS5Failed login attempts per IP before lockout (0 = disabled)
MALLARD_LOGIN_LOCKOUT300Lockout duration in seconds after exceeding max login attempts
MALLARD_LOG_FORMATtextLog format: text or json
MALLARD_GDPR_MODEfalseEnable GDPR-friendly preset (see PRIVACY.md)
MALLARD_STRIP_REFERRER_QUERYfalseStrip ?query and #fragment from stored referrers
MALLARD_ROUND_TIMESTAMPSfalseRound event timestamps to the nearest hour
MALLARD_SUPPRESS_VISITOR_IDfalseReplace HMAC visitor hash with per-request UUID (breaks unique-visitor counting)
MALLARD_SUPPRESS_BROWSER_VERSIONfalseStore browser name only, not version
MALLARD_SUPPRESS_OS_VERSIONfalseStore OS name only, not version
MALLARD_SUPPRESS_SCREEN_SIZEfalseOmit screen width and device type
MALLARD_GEOIP_PRECISIONcityGeoIP precision: city, region, country, or none

API Reference

All /api/stats/*, /api/keys/*, and /api/stats/export endpoints require authentication (session cookie or API key). The ingestion endpoint and health checks are unauthenticated.

Common Query Parameters

ParameterDefaultDescription
site_id(required)Analytics property identifier
period30dTime period: day, today, 7d, 30d, 90d
start_date(none)Explicit start date (YYYY-MM-DD)
end_date(none)Explicit end date (YYYY-MM-DD)
limit10Result limit (breakdowns only, max 1000)

Endpoints

Health and Monitoring

MethodEndpointDescription
GET/healthLiveness check (returns ok)
GET/health/readyReadiness probe — queries DuckDB; returns 503 if not ready
GET/health/detailedJSON system status (version, buffer, auth, GeoIP, behavioral extension, cache)
GET/metricsPrometheus metrics (text/plain; version=0.0.4)
GET/robots.txtCrawler policy
GET/.well-known/security.txtRFC 9116 security contact

Authentication

MethodEndpointDescription
POST/api/auth/setupFirst-run admin password setup
POST/api/auth/loginLogin with credentials
POST/api/auth/logoutLogout and clear session
GET/api/auth/statusCheck authentication status

Ingestion

MethodEndpointDescription
POST/api/eventIngest a tracking event (permissive CORS, 64 KB body limit)
GET/api/eventPixel tracking — same parameters via query string; returns 1×1 GIF

Core Analytics (authenticated)

MethodEndpointDescription
GET/api/stats/mainUnique visitors, pageviews, bounce rate, avg session duration
GET/api/stats/timeseriesTime-bucketed visitor and pageview counts
GET/api/stats/breakdown/{dim}Breakdown by: pages, sources, browsers, os, devices, countries

Advanced Analytics (authenticated, requires behavioral extension)

MethodEndpointParametersDescription
GET/api/stats/sessionsSession metrics (total, avg duration, pages/session)
GET/api/stats/funnelsteps, windowMulti-step conversion funnel
GET/api/stats/retentionweeks (1–52)Weekly retention cohort grid
GET/api/stats/sequencessteps (min: 2)Behavioral sequence pattern matching
GET/api/stats/flowpageNext-page flow analysis

Data Management (authenticated)

MethodEndpointDescription
GET/api/stats/exportExport analytics data (format=csv or format=json)
DELETE/api/gdpr/eraseErase all events for a site within a date range (Art. 17 erasure)
POST/api/keysCreate an API key
GET/api/keysList all API keys
DELETE/api/keys/{hash}Revoke an API key

Architecture

flowchart TD
    TS["Tracking Script\nmallard.js &lt;1KB"]
    DASH["Dashboard SPA\nPreact + HTM"]

    TS -->|"POST /api/event"| AXUM
    DASH <-->|"GET /api/stats/*"| AXUM

    subgraph BINARY["Single Binary — Single Process"]
        AXUM["Axum HTTP Server\nport 8000"]

        subgraph INGEST["Ingestion Pipeline"]
            direction LR
            OC["Origin + Rate Limit"] --> BF["Bot Filter + UA Parser"]
            BF --> GEO["GeoIP + Visitor ID Hash"]
            GEO --> BUF["In-Memory Buffer"]
        end

        subgraph STORE["Two-Tier Storage"]
            direction LR
            DB["DuckDB disk-based\nmallard.duckdb"] -->|"COPY TO"| PQ["Parquet Files\ndate-partitioned ZSTD"]
            DB --> VIEW["events_all VIEW\nhot union cold"]
            PQ -->|"read_parquet()"| VIEW
        end

        subgraph QUERY["Query Engine"]
            direction LR
            CACHE["TTL Query Cache"] --> QH["Stats + Sessions\nFunnels + Retention\nSequences + Flow"]
        end

        AXUM --> OC
        BUF -->|"flush"| DB
        VIEW --> CACHE
        QH --> AXUM
    end

Module Map

ModulePurpose
config.rsTOML + environment variable configuration
server.rsAxum router, middleware stack, route registration
ingest/handler.rsPOST /api/event ingestion handler
ingest/buffer.rsIn-memory event buffer with periodic flush
ingest/visitor_id.rsHMAC-SHA256 privacy-safe visitor ID generation
ingest/useragent.rsUser-Agent parsing (browser, OS, version, device)
ingest/geoip.rsMaxMind GeoLite2 reader with graceful fallback
ingest/ratelimit.rsPer-site token-bucket rate limiter
storage/schema.rsDuckDB table definitions, events_all view, behavioral extension loading
storage/parquet.rsParquet write, read, and date-partitioning
storage/migrations.rsSchema versioning
query/metrics.rsCore metric calculations (visitors, pageviews, bounce rate)
query/breakdowns.rsDimension breakdown queries
query/timeseries.rsTime-bucketed aggregations
query/sessions.rssessionize()-based session queries
query/funnel.rswindow_funnel() conversion funnel builder
query/retention.rsretention() cohort query execution
query/sequences.rssequence_match() pattern query execution
query/flow.rssequence_next_node() flow analysis
query/cache.rsTTL-based query result cache
api/stats.rsAll analytics API handlers
api/errors.rsAPI error types and HTTP responses
api/auth.rsOrigin validation, session auth, API key management
dashboard/Embedded SPA (Preact + HTM, no build step)

Dashboard

The dashboard is a single-page application built with Preact + HTM, embedded directly in the binary via rust-embed. No build step or Node.js required.

Mallard Metrics Dashboard

Views include:

  • Visitor and pageview counts with period selector
  • Time-series line chart (visitors and pageviews)
  • Six breakdown tables (pages, sources, browsers, OS, devices, countries)
  • Session analytics cards (total sessions, avg duration, pages/session)
  • Funnel analysis visualization (horizontal bar chart)
  • Retention cohort grid (weekly cohort boolean matrix)
  • Sequence matching conversion metrics
  • Flow analysis (next-page navigation table)
  • CSV and JSON export buttons

Technology Stack

ComponentTechnologyVersion
LanguageRust1.94.0 (MSRV)
Web FrameworkAxum0.8
DatabaseDuckDB (disk-based, embedded)1.5.x (crate 1.10501.0)
Analytics Enginebehavioral extensionruntime-loaded
Storage FormatParquet (ZSTD compressed)date-partitioned
FrontendPreact + HTMno build step
Password HashingArgon2idargon2 0.5
GeoIPMaxMind GeoLite2maxminddb 0.27
DeploymentStatic musl binaryFROM scratch Docker

Development

Prerequisites

  • Rust 1.94.0+ (managed automatically via rust-toolchain.toml)
  • Git

Build and Test

# Build
cargo build

# Run all tests (333 total: 262 unit + 71 integration)
cargo test

# Clippy (zero warnings required)
cargo clippy --all-targets

# Format check
cargo fmt -- --check

# Build documentation
cargo doc --no-deps

# Run the server
cargo run

# Run benchmarks
cargo bench

Quality Standards

  • Zero clippy warnings — pedantic, nursery, and cargo lint groups enabled
  • Zero formatting violations — enforced via cargo fmt
  • All 333 tests pass — no ignored tests
  • Documentation builds without errors

See CONTRIBUTING.md for the full development workflow.


Deployment

Create a .env file (do not commit to source control):

MALLARD_SECRET=your-random-32-char-secret
MALLARD_ADMIN_PASSWORD=your-strong-dashboard-password
MALLARD_SECURE_COOKIES=true
MALLARD_METRICS_TOKEN=your-prometheus-bearer-token

Then start:

docker compose up -d

Reverse Proxy (nginx)

server {
    listen 443 ssl;
    server_name analytics.example.com;

    ssl_certificate     /etc/ssl/certs/analytics.example.com.crt;
    ssl_certificate_key /etc/ssl/private/analytics.example.com.key;

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

GeoIP Setup (optional)

  1. Register for a free MaxMind account at maxmind.com
  2. Download the GeoLite2-City database (.mmdb format)
  3. Set MALLARD_GEOIP_DB=/path/to/GeoLite2-City.mmdb

If the GeoIP database is missing, country/region/city fields are stored as NULL. The system degrades gracefully — no errors are raised.


Documentation

DocumentDescription
GitHub PagesFull documentation site — API reference, architecture, deployment, security
PRIVACY.mdData-processing architecture, GDPR/ePrivacy/CCPA analysis, operator compliance obligations
CONTRIBUTING.mdDevelopment setup, workflow, code standards, PR checklist
SECURITY.mdSecurity model, privacy guarantees, threat model, vulnerability reporting
CHANGELOG.mdVersion history following Keep a Changelog format
ROADMAP.mdImplementation phases, completed work, and future plans
PERF.mdBenchmark framework, methodology, and measured baselines
LESSONS.md21 development lessons learned, organized by category
DEVELOPMENT.mdModule map, build commands, and development guidelines
mallard-metrics.toml.exampleAnnotated configuration template

License

AGPL-3.0 — see LICENSE for the full text.

Mallard Metrics is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.