kiko

module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 20, 2026 License: MIT

README

kiko — Privacy-first web analytics collector in Go

High-performance, cookie-free, batteries-included server-side analytics.

Version Release CI Security CodeQL codecov gghstats clones Go 1.26.4 pkg.go.dev Go Report Card deps.dev

Repo: github.com/hrodrig/kiko · Releases: Releases

Early development: kiko is in initial active development. Expect breaking changes, incomplete features, and data loss between releases. Do not use in production.

Privacy-first web analytics collector in Go. A ~500-byte tracking script sends inbound page views; the server hashes visitors without cookies, buffers hits in memory, and flushes sorted batch inserts plus hourly aggregations to SQLite (default), PostgreSQL, or MySQL.

This repo is kiko — collection only. kui (kiko + ui) is the analytics user interface in a separate repo: charts, tables, and reports over kiko's query API. kui is not built yet; the hero window on the right is where kui will sit.

Self-hosted deployment (Docker Compose, Helm, Kubernetes manifests): kiko-selfhosted — production paths and example stacks live there; this repo ships the application binary, packages, and container image only (same split as pgwd / pgwd-selfhosted).

GitHub repo traffic (history beyond 14 days): sibling tool gghstatslive stats for kiko (clone badge above).

Documentation: SPECIFICATIONS.md (architecture, schema, API), ROADMAP.md (phases), configs/kiko.yml.sample, and man kiko (when packaged).

Brand assets: hero banner assets/kiko-hero-full.png (compact assets/kiko-hero.png + source assets/kiko-hero.svg); favicons under assets/favicons/ (favicon.svg + PNG/ICO + manifest.json for future kui). The collector API does not serve these yet.

Table of contents


Quick start

git clone https://github.com/hrodrig/kiko
cd kiko
make build
./kiko serve

Add to your site HTML (replace the host with your public kiko URL):

<script defer src="https://analytics.yourdomain.com/kiko.js"></script>

Custom config:

kiko serve -c /etc/kiko/kiko.yml

Probes for Kubernetes: GET /api/v1/healthz (liveness), GET /api/v1/readyz (readiness — DB + buffer). Deployment paths (Compose, Helm, MicroK8s): kiko-selfhosted.

↑ Back to top


Why kiko

  • No cookies — SHA-256 visitor hash with daily salt. No GDPR banner needed.
  • Batteries included — ~500B tracking script, in-memory buffer, batch flush, migrations, and health probes in one static binary.
  • No Node in production — tracking script is ~500 bytes JS. Server is a static Go binary.
  • Passes audits — govulncheck, grype, gocyclo, cover. Same standard as sibling projects.
  • Single binary — Go, CGO disabled, distroless. ~2.5MB compiled.
  • Multi-database — SQLite zero-config default; Postgres and MySQL via config.

↑ Back to top


Design principles

The hero diagram is the product thesis in one glance: a Go collector built to do one job — gather web analytics on infrastructure you control, without turning visitors into tracking targets.

Light and fast. A ~500-byte tracking script feeds a compact Go binary tuned for throughput. Inbound hits are validated and hashed quickly; there is no heavyweight client SDK and no JavaScript runtime on the server — just a small static binary that chews through traffic like focused industrial gear.

Privacy by default. The whole pipeline sits inside a cookie-free boundary. Visitors become ephemeral daily hashes, not persistent profiles; IP addresses stay in memory and never reach disk. Measurement without surveillance — analytics you own, not cross-site fingerprinting.

Buffer, then batch. Hits accumulate in a visible in-memory buffer first. On a fixed interval they are sorted, aggregated, and flushed as organized batch inserts plus hourly rollups — the ordered blocks in the diagram. Hot path stays simple; database work stays predictable.

Your database, your choice. One collector, three backends: SQLite (zero-config default), PostgreSQL, or MySQL. Same schema and flush logic — pick the engine that fits your stack without forking the application.

That focus — fast ingestion, respectful privacy, smart batching, portable storage — is what kiko is for: a robust Go backend dedicated to collecting analytics sovereignly and respectfully. kui (kiko + ui) handles the UI in a separate project; kiko does not ship charts or admin screens.

↑ Back to top


How it works

The hero diagram maps this pipeline end to end: inbound raw data from the browser, buffered hits in the Go collector, sorted and aggregated batch writes, then SQLite / PostgreSQL / MySQL — read by kui (the UI, separate repo, future).

flowchart LR
    subgraph browser["Browser"]
        JS["kiko.js (~500B)"]
    end

    subgraph kiko["kiko (Go binary)"]
        direction TB
        IN["POST /hit · GET /hit.gif"]
        RL["rate limit (per IP)"]
        HASH["visitor_hash<br/>SHA-256(ip + ua + daily salt)"]
        BUF["MemBuffer<br/>(mutex, cap 4k)"]
        FLUSH["flush loop<br/>every 10s"]
        INSERT["BatchInserter + aggregations"]

        IN --> RL --> HASH --> BUF --> FLUSH --> INSERT
    end

    JS --> IN
    INSERT --> DB[("SQLite / PostgreSQL / MySQL")]
    DASH["kui (UI, separate repo)"] -.-> DB

Each hit is validated, hashed, and appended to an in-memory buffer (mutex-protected; drops when full). Every 10s the buffer flushes to the database in batch: raw hits, normalized paths/referrers, and hourly counts. Rate limiting uses a per-IP token bucket (pattern from gghstats).

↑ Back to top


Configuration

Config file: kiko.yml (see configs/kiko.yml.sample). All fields overridable via env vars with KIKO_ prefix.

listen: ":8080"
public_url: "https://analytics.yourdomain.com"
log_level: info

database:
  driver: sqlite          # sqlite | postgres | mysql
  path: ./data/kiko.db    # sqlite only

buffer:
  flush_interval: 10      # seconds between batch flushes
  capacity: 4096          # max hits in memory before drop

rate_limit:
  enabled: true
  requests_per_sec: 100
  burst: 200
  host_requests_per_sec: 50   # optional per-host ingest cap
  host_burst: 100

filter:
  trust_proxy: false          # use first public IP from X-Forwarded-For / X-Real-IP
  block_bots: true
  block_prefetch: true
  block_referrer_spam: true
  block_datacenter_ips: false
  ignore_ips: []              # never count these client IPs
  extra_datacenter_cidrs: []

allowed_hosts: []         # empty = accept all

visitor:
  salt: ""                # set in production (KIKO_VISITOR_SALT)
Env Maps to
KIKO_LISTEN listen
KIKO_PUBLIC_URL public_url
KIKO_LOG_LEVEL log_level
KIKO_DATABASE_DRIVER database.driver
KIKO_DATABASE_PATH database.path
KIKO_DATABASE_HOST database.host
KIKO_DATABASE_PORT database.port
KIKO_DATABASE_USER database.user
KIKO_DATABASE_PASSWORD database.password
KIKO_DATABASE_DBNAME database.dbname
KIKO_DATABASE_SSLMODE database.sslmode
KIKO_DATABASE_DSN database.dsn (overrides all)
KIKO_BUFFER_FLUSH_INTERVAL buffer.flush_interval
KIKO_BUFFER_CAPACITY buffer.capacity
KIKO_RATE_LIMIT_ENABLED rate_limit.enabled
KIKO_RATE_LIMIT_REQUESTS_PER_SEC rate_limit.requests_per_sec
KIKO_RATE_LIMIT_BURST rate_limit.burst
KIKO_RATE_LIMIT_HOST_REQUESTS_PER_SEC rate_limit.host_requests_per_sec
KIKO_RATE_LIMIT_HOST_BURST rate_limit.host_burst
KIKO_FILTER_TRUST_PROXY filter.trust_proxy
KIKO_FILTER_BLOCK_BOTS filter.block_bots
KIKO_FILTER_BLOCK_PREFETCH filter.block_prefetch
KIKO_FILTER_BLOCK_REFERRER_SPAM filter.block_referrer_spam
KIKO_FILTER_BLOCK_DATACENTER_IPS filter.block_datacenter_ips
KIKO_ALLOWED_HOSTS allowed_hosts (comma-separated)
KIKO_VISITOR_SALT visitor.salt
Log levels
Level Value Description
trace 0 Diagnostic detail, most verbose
debug 1 Debugging information
info 2 General operational messages (default)
warn 3 Non-critical issues
error 4 Runtime errors
fatal 5 Critical failure, process exits
off 6 Nothing logged

↑ Back to top


Build & releases

Development (this repo):

make build
sudo cp kiko /usr/local/bin/   # optional

Production deployment (Compose, Helm, Kubernetes, env layout): kiko-selfhosted — not this repo.

Published artifacts: GitHub Releases and ghcr.io/hrodrig/kiko (multi-arch). Formats below are built by CI/Goreleaser from this repo; runbooks live in kiko-selfhosted.

OS Arch Format
Linux amd64, arm64 tar.gz, .deb, .rpm, Docker
macOS amd64, arm64 tar.gz, Homebrew
Windows amd64, arm64 zip
FreeBSD amd64, arm64 tar.gz, port
OpenBSD amd64, arm64 tar.gz, port

↑ Back to top


Tracking snippet

The server embeds kiko.js (~500B). It sends POST /hit via navigator.sendBeacon() and falls back to a 1×1 GIF pixel (GET /hit.gif) when needed. Always responds with a transparent GIF — success and rejection look the same to the browser.

SPAs: pageviews fire on history.pushState / popstate (History API). For hash-based routers, load with ?hash=1 to also track hashchange.

↑ Back to top


API

Endpoint Method Description
/kiko.js GET Tracking script (cached 24h)
/hit POST JSON tracking endpoint
/hit.gif GET Pixel fallback
/api/v1/healthz GET Liveness probe
/api/v1/readyz GET Readiness probe (DB + buffer)
/api/v1/stats/summary GET Headline metrics (hits, uniques, top path)
/api/v1/stats/paths GET Top paths
/api/v1/stats/refs GET Top referrers
/api/v1/stats/timeline GET Time series (interval=hour|day)
/api/v1/stats/visitors GET Unique visitors
/api/v1/stats/channels GET Breakdown by channel
/api/v1/stats/browsers GET Breakdown by browser
/api/v1/stats/os GET Breakdown by OS
/api/v1/stats/utm GET Breakdown by utm_source
Stats API (for kui)

All stats endpoints accept:

Query Required Description
host yes Site hostname
since no Start (YYYY-MM-DD or RFC3339). Default: 30 days before until.
until no End (same formats). Default: now.
limit no Max rows for list endpoints (default 10, max 100).
interval no Timeline only: hour or day (default day).

Auth: set api.key / KIKO_API_KEY. Send X-API-Key: <key> or Authorization: Bearer <key>. Empty key = open (dev only; server logs a warning).

Example

curl -sS 'http://127.0.0.1:8080/api/v1/stats/summary?host=gghstats.com&since=2026-01-01' \
  -H 'X-API-Key: your-secret'

Responses are JSON with Cache-Control: public, max-age=60.

Hit payload

The browser sends only the fields below. kiko enriches each hit server-side (visitor hash, browser/OS, traffic channel) before buffering — those are not part of the client JSON.

POST /hit

Headers

Header Required Notes
Content-Type yes application/json
User-Agent recommended Used for bot filtering, daily visitor hash, and browser/OS labels

JSON body

Field Type Required Description
host string yes Site hostname (e.g. gghstats.com). Must match allowed_hosts when configured.
path string no Page path + query string. Default / if empty.
referrer string no Full referrer URL or empty for direct traffic.
title string no Document title (document.title).
width number no Screen width in pixels (screen.width).

Example

{
  "host": "gghstats.com",
  "path": "/blog/my-post",
  "referrer": "https://dev.to/someone",
  "title": "My Post | GGHStats",
  "width": 1920
}

Try it

curl -sS -X POST 'http://127.0.0.1:8080/hit' \
  -H 'Content-Type: application/json' \
  -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0.0.0' \
  -d '{"host":"localhost","path":"/demo","referrer":"https://google.com/search?q=kiko","title":"Demo","width":1920}' \
  -o /dev/null -w '%{http_code} %{content_type}\n'

Expect 200 image/gif (43-byte transparent GIF). Invalid or rejected hits return the same GIF so browsers cannot distinguish success from drop.

Response headers (ingest debugging):

Header When Meaning
X-Kiko-Dropped: 1 rejected hit Silent drop (bot, prefetch, spam referrer, datacenter IP, rate limit, …)
X-Debug-Request: true (request) optional Response body is JSON with client_ip, accepted, and reason instead of GIF

Use debug mode behind a reverse proxy to verify client IP forwarding without polluting stats — see kiko-selfhosted for Traefik/Ingress examples.

GET /hit.gif

Pixel fallback when sendBeacon is unavailable. Query params map to the same JSON fields:

Param JSON field Description
h host Site hostname
p path Page path
r referrer Referrer URL
t title Document title
w width Screen width (integer)

Example

GET /hit.gif?h=gghstats.com&p=%2Fblog%2Fmy-post&r=https%3A%2F%2Fdev.to%2Fsomeone&t=My%20Post&w=1920

Same User-Agent and client IP rules as POST /hit.

Server-side enrichment (not in client payload)
Stored field Source
visitor_hash SHA-256 of client IP + User-Agent + daily salt (IP never written to disk)
browser, os Parsed from User-Agent (internal/ua/)
channel Classified from referrer + host (direct, organic, social, email, referral) — internal/ref/
source Display label for referrer (Google, Twitter/X, …)
utm_* Parsed from path query (utm_source, …); stripped from stored path
referrer (stored) Normalized URL (query/fragment stripped)

Full API reference: SPECIFICATIONS.md §4.

↑ Back to top


Quality gates

Gate Threshold Enforced
gofmt -s No diff CI + release
go vet 0 warnings CI + release
gocyclo ≤ 14 CI + release
govulncheck 0 vulnerabilities CI + release
grype 0 high/critical CI + release
go test -cover ≥ 80% CI + release
CodeQL Clean CI

Local check: make release-check

↑ Back to top


Project Role
kiko-selfhosted Docker Compose, Helm, K8s manifests
kui Analytics UI (kiko + ui) — charts and reports over kiko's API (planned)
gghstats GitHub traffic stats
pgwd PostgreSQL connection watchdog
kzero Kubernetes pipeline CLI
groot Kubernetes log collector

↑ Back to top


Get involved

Found kiko useful? You can:

↑ Back to top


Star History

Star History Chart

↑ Back to top


License

MIT — LICENSE

↑ Back to top

Directories

Path Synopsis
cmd
kiko command
internal
cli
hit
log
Package log provides leveled logging matching Microsoft LogLevel semantics.
Package log provides leveled logging matching Microsoft LogLevel semantics.
ref
Package ref normalizes referrer URLs and assigns a coarse traffic channel.
Package ref normalizes referrer URLs and assigns a coarse traffic channel.
ua
Package ua parses User-Agent strings into browser and OS names without regex.
Package ua parses User-Agent strings into browser and OS names without regex.
utm
Package utm extracts campaign parameters from page paths.
Package utm extracts campaign parameters from page paths.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL