A privacy-focused, self-hosted web analytics platform written in Common Lisp.
- Privacy-first - No cookies, no personal data collection
- Self-hosted - Your data stays on your server
- Automatic HTTPS - Built-in Let's Encrypt certificate management via pure-tls
- Country detection - Built-in IP-to-country lookup (no external API calls)
- Lightweight - Single binary, SQLite database, runs on free-tier cloud VMs (AWS, GCP, Azure, etc.)
- Simple setup - Interactive wizard or command-line options
makeInteractive setup wizard:
./happening setupOr non-interactive:
./happening setup \
-a admin \
-P yourpassword \
-u https://analytics.example.com \
-e [email protected]# HTTP only (development)
./happening -p 8080
# HTTPS with automatic Let's Encrypt certificates
./happening -u https://analytics.example.com| Option | Description |
|---|---|
-p, --port PORT |
HTTP server port (default: 8080) |
-u, --url URL |
Public base URL (enables HTTPS if https://) |
-d, --database PATH |
SQLite database path |
-s, --slynk-port PORT |
Start Slynk server for remote REPL |
| Variable | Description |
|---|---|
ACME_EMAIL |
Contact email for Let's Encrypt account |
ACME_STAGING |
Set to true for staging certificates (default: production) |
ACME_CERT_PATH |
Custom certificate storage path |
ACME_RENEWAL_DAYS |
Days before expiry to trigger renewal (default: 30) |
Environment variables can also be set in a .env file.
When you specify an https:// URL with -u, Happening automatically:
- Obtains a Let's Encrypt certificate using TLS-ALPN-01 challenge
- Serves HTTPS on port 443
- Renews certificates automatically before expiry
Requirements:
- Port 443 must be accessible from the internet
- Domain must resolve to your server's IP address
Example deployment:
# First time setup
./happening setup -a admin -P secret123 \
-u https://analytics.example.com \
-e [email protected]
# Run with production certificates (default)
[email protected] \
./happening -u https://analytics.example.comAfter setup, add this script to your website:
<script defer src="https://analytics.example.com/js/script.js"
data-api="https://analytics.example.com/api/event"></script>Country data from ipverse/country-ip-blocks is embedded in the binary—no setup required. To use fresher data, clone the repo:
git clone https://github.com/ipverse/country-ip-blocks data/country-ip-blocksExternal data takes priority over embedded data. See docs/GEOIP-SETUP.md for details.
- ipverse/country-ip-blocks - IP-to-country data (CC0 1.0 Public Domain)
- SBCL (Steel Bank Common Lisp)
- Make
make clean && make./happening -p 8080 -s 4005Then connect from Emacs with M-x sly-connect.
MIT License
Copyright (c) 2025 Anthony Green