Web Analytics in a single binary.
HitKeep is a self-hostable, privacy-first web analytics platform designed for radical simplicity without sacrificing performance.
Unlike other solutions that require you to manage a complex stack (PostgreSQL, Redis, ClickHouse, Nginx), HitKeep runs as a single, self-contained executable. It embeds a high-performance OLAP database (DuckDB) and a distributed message queue (NSQ) directly into the binary.
HitKeep Cloud is coming!
Prefer a managed solution and funding Open Source? Join the Early Access Waitlist for fully managed, data-sovereign and privacy-first analytics in the EU or US.
Visit hitkeep.com for the complete documentation, including:
- Single Binary Runtime: No external database, queue, or cache to provision.
- Embedded DuckDB + NSQ: Columnar analytics storage with in-process burst buffering.
- Privacy-First Tracking: Cookie-less by default, bot filtering, DNT support, optional
sendBeacondisable. - Analytics Coverage: Traffic overview, raw hits, events, goals, funnels, and UTM attribution fields.
- Security & Auth: JWT sessions, remember-me tokens, password reset, TOTP MFA, and WebAuthn passkeys.
- RBAC & Team Management: Instance roles and per-site roles with delegated permissions.
- API Clients: Create scoped API tokens for automation and integrations.
- Share Links: Read-only dashboard sharing for stakeholders.
- Data Lifecycle: Per-site retention, scheduled archival to Parquet, and on-demand user/site takeout exports.
- Ops Endpoints: Health/readiness probes and versioned OpenAPI endpoint.
- Cluster Support: Optional leader/follower topology via memberlist gossip.
Head over to Releases and download the binary for your system, for example:
$ wget https://github.com/PascaleBeier/hitkeep/releases/latest/download/hitkeep-linux-arm64
$ chmod +x hitkeep-linux-arm64Security Tip: Avoid passing secrets (like
JWT_SECRET) via flags in production, as they appear in process lists. Use theHITKEEP_JWT_SECRETenvironment variable instead.
# Set secret via ENV, config via flags
$ export HITKEEP_JWT_SECRET="your-secure-random-string"
$ ./hitkeep-linux-arm64 -public-url="https://analytics.example.org"
# to use your public ip
$ ./hitkeep-linux-arm64 -public-url="http://1.2.3.4:8080"Also see examples.
- Create a
compose.ymlfile:
services:
hitkeep:
image: ghcr.io/pascalebeier/hitkeep:latest
container_name: hitkeep
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- hitkeep_data:/var/lib/hitkeep/data
environment:
# Securely pass secrets via ENV
- HITKEEP_JWT_SECRET=replace-this-with-a-long-random-string
command:
# IMPORTANT: Set this to your actual public domain in production
- "-public-url=http://localhost:8080"
volumes:
hitkeep_data: {}-
Run it:
docker compose up -d
-
Open
http://localhost:8080to create your admin account.
Once your instance is running and you have created a website in the dashboard, add this script to the <head> of your website:
<script async src="https://your-hitkeep-instance.com/hk.js"></script>To ignore DNT:
<script
async
src="https://your-hitkeep-instance.com/hk.js"
data-collect-dnt="true"
></script>To use fetch over navigator.sendBeacon:
<script
async
src="https://your-hitkeep-instance.com/hk.js"
data-collect-dnt="true"
data-disable-beacon="true"
></script>Emit custom events from your app:
<script>
window.hk = window.hk || {};
window.hk.event?.("signup", { plan: "pro", source: "landing-page" });
</script>HitKeep is configured via command-line flags or environment variables. Flags take precedence.
| Flag | Environment Variable | Why it matters most in production |
|---|---|---|
-public-url |
HITKEEP_PUBLIC_URL |
Used for JWT issuer/audience and CORS behavior. |
-jwt-secret |
HITKEEP_JWT_SECRET |
Signs auth tokens; must be stable and secret across restarts. |
-db |
HITKEEP_DB_PATH |
Controls where hitkeep.db is stored/persisted. |
-archive-path |
HITKEEP_ARCHIVE_PATH |
Stores takeout and retention archives. |
-trusted-proxies |
HITKEEP_TRUSTED_PROXIES |
Controls whether forwarded headers are trusted for real IP and GeoIP. |
-retention-days |
HITKEEP_DATA_RETENTION_DAYS |
Default retention policy for new sites. |
-trusted-proxiesnow defaults to*(trust-all CIDRs). Set this explicitly in production to your reverse proxy/load balancer CIDRs.-jwt-secretis auto-generated if omitted. That is fine for local dev but will invalidate sessions on restart unless you persist a fixed secret.
| Flag | Environment Variable | Default | Description |
|---|---|---|---|
-public-url |
HITKEEP_PUBLIC_URL |
http://localhost:8080 |
Public URL for JWT issuer/audience and CORS. Set this to your real public domain. |
-jwt-secret |
HITKEEP_JWT_SECRET |
(random) | Secret key for signing auth tokens. Use a fixed strong value in production. |
-http |
HITKEEP_HTTP_ADDR |
:8080 |
Address to bind the HTTP server to. |
-healthcheck |
- | false |
Run one-shot healthcheck mode and exit (useful for container probes). |
-db |
HITKEEP_DB_PATH |
hitkeep.db |
Path to the DuckDB database file. |
-log-level |
HITKEEP_LOG_LEVEL |
info |
Logging verbosity (debug, info, warn, error). |
| Flag | Environment Variable | Default | Description |
|---|---|---|---|
-archive-path |
HITKEEP_ARCHIVE_PATH |
archive |
Directory for exports, rollups, and archival artifacts. |
-retention-days |
HITKEEP_DATA_RETENTION_DAYS |
365 |
Default data retention window (days) for newly created sites. |
| Flag | Environment Variable | Default | Description |
|---|---|---|---|
-mail-driver |
HITKEEP_MAIL_DRIVER |
smtp |
Mail driver to use (smtp or log). |
-mail-host |
HITKEEP_MAIL_HOST |
SMTP Server Hostname (e.g., smtp.postmarkapp.com). |
|
-mail-port |
HITKEEP_MAIL_PORT |
587 |
SMTP Server Port. |
-mail-username |
HITKEEP_MAIL_USERNAME |
SMTP Username. | |
-mail-password |
HITKEEP_MAIL_PASSWORD |
SMTP Password. | |
-mail-encryption |
HITKEEP_MAIL_ENCRYPTION |
tls |
Encryption mode: tls (STARTTLS), ssl (Implicit), or none. |
-mail-insecure-skip-verify |
HITKEEP_MAIL_INSECURE_SKIP_VERIFY |
false |
Skip TLS certificate validation (useful for self-signed certs). |
-mail-from-address |
HITKEEP_MAIL_FROM_ADDRESS |
hitkeep@localhost |
The email address messages are sent from. |
-mail-from-name |
HITKEEP_MAIL_FROM_NAME |
HitKeep |
The name displayed to the recipient. |
| Flag | Environment Variable | Default | Description |
|---|---|---|---|
-ingest-rate |
HITKEEP_INGEST_RATE_LIMIT |
20.0 |
Rate limit for /ingest (req/sec/ip). |
-ingest-burst |
HITKEEP_INGEST_BURST |
40 |
Burst size for /ingest. |
-api-rate |
HITKEEP_API_RATE_LIMIT |
10.0 |
Rate limit for general API endpoints (req/sec/ip). |
-api-burst |
HITKEEP_API_BURST |
20 |
Burst size for general API. |
-auth-rate |
HITKEEP_AUTH_RATE_LIMIT |
2.0 |
Rate limit for login/signup (req/sec/ip). |
-auth-burst |
HITKEEP_AUTH_BURST |
5 |
Burst size for login/signup. |
Use this when HitKeep is behind a reverse proxy or load balancer and you want to trust forwarded headers. This affects both rate limiting and GeoIP resolution.
| Flag | Environment Variable | Default | Description |
|---|---|---|---|
-trusted-proxies |
HITKEEP_TRUSTED_PROXIES |
"*" |
Comma-separated trusted proxy CIDRs or * (trust all). Used for client IP and GeoIP resolution. |
Behavior:
*trusts forwarding headers from any direct peer.- CIDR list trusts forwarding headers only when the direct connection IP is in the trusted list.
- Empty disables trusted proxy behavior and uses the direct remote address.
| Flag | Environment Variable | Default | Description |
|---|---|---|---|
-name |
HITKEEP_NODE_NAME |
hostname-timestamp |
Unique name for this node in the cluster. |
-bind |
HITKEEP_BIND_ADDR |
0.0.0.0:7946 |
Bind address for cluster gossip (Memberlist). |
-join |
HITKEEP_JOIN_ADDR |
"" |
Address of a peer node to join. |
-nsq-tcp-address |
HITKEEP_NSQ_TCP_ADDRESS |
127.0.0.1:4150 |
Address of the internal embedded NSQ TCP. |
-nsq-http-address |
HITKEEP_NSQ_HTTP_ADDRESS |
127.0.0.1:4151 |
Address of the internal embedded NSQ HTTP. |
As of now, without any parqueting, you can expect to store 1 Million Raw Hits per ~120MB.
HitKeep bridges the gap between simple log analyzers (like GoAccess) and enterprise analytics (like Umami/Plausible).
- Ingestion: Requests hit the Go HTTP server.
- Buffering: Events are published to an embedded NSQ topic (
hits) in memory. This decouples the API from the database write speed. - Storage: An internal consumer creates micro-batches and writes them to DuckDB, a columnar database that lives in a single file but offers OLAP performance comparable to ClickHouse.
- Clustering: Nodes communicate via Gossip protocol. The Leader node handles database writes, while Follower nodes proxy ingestion traffic to the leader.
- Go 1.26+
- Node.js 24+
- Make
# Clone the repo
git clone https://github.com/pascalebeier/hitkeep.git
cd hitkeep
# Build frontend and backend
make build
# Run the binary
./hitkeepWe use SemVer and Conventional Commits.
See CHANGELOG.md.
Distributed under the MIT License. See LICENSE for more information.
