Skip to content

Repository files navigation

watchpost

License: AGPL v3 Rust 1.88+ Latest release Docker pulls GitHub stars

Self-hosted GitHub repo metrics that outlive GitHub's 14-day traffic window.

Please leave a ⭐ star if watchpost is useful - it helps others find it :).

Demo

A watchpost repo page in dark mode: stat tiles for stars, views, clones and downloads over a full-width star chart spanning 90 days, promo event markers dotted along its top.

One page per repo - the metrics on top, the events that caused them marked on every chart.

GitHub throws your traffic data away after 14 days. watchpost samples it hourly into a local SQLite file and keeps it, next to a timeline of what you did to earn it: post a release to Hacker News, add it as an event, and the spike lands under a marker instead of being a spike you no longer remember the cause of. It talks to nothing but the GitHub API.

Install

1. Install Docker

watchpost runs in Docker. Get it at get.docker.com, or install Docker Desktop on macOS/Windows. Make sure it's running before you continue.

2. Install watchpost

Linux / macOS:

curl -fsSL https://raw.githubusercontent.com/0xzerolight/watchpost/main/scripts/install.sh | bash

Windows (PowerShell):

irm https://raw.githubusercontent.com/0xzerolight/watchpost/main/scripts/install.ps1 | iex

This pulls the image, starts the container on http://127.0.0.1:8080, and opens the setup page. Both scripts run code fetched from this repository - read the note at the top of either one, and pin a release tag with WATCHPOST_REF if you would rather not track main.

3. Paste a GitHub token

The setup page asks for a fine-grained personal access token and checks it against GitHub before saving it. Under Repository permissions grant:

Permission What it buys you
Metadata: read The repository list and the basic counts. Selected for you, cannot be removed
Administration: read Traffic - views, clones, referrers, popular paths
Contents: read Releases and asset download counts
Pull requests: read The open pull request count

A classic token with the repo scope also works. A missing permission only costs that one part of a sync, not the whole sync - without Administration: read the traffic charts stay empty while everything else still lands. Traffic is only served for repositories you own or administer, whatever the token says.

Collection starts as soon as the token is saved, so the repo list appears within a minute; traffic data follows on the same pass. Pick which repos to track on the Settings page - nothing is tracked until you say so.

Manual install (without the script)

Prebuilt image - the same image the script uses, you just supply the compose file:

mkdir -p watchpost/data && cd watchpost
curl -fsSL https://raw.githubusercontent.com/0xzerolight/watchpost/main/compose.prod.yml -o docker-compose.yml
printf 'PUID=%s\nPGID=%s\n' "$(id -u)" "$(id -g)" > .env   # only if your host uid is not 1000
docker compose up -d

Build from source - no prebuilt image, builds from the Dockerfile:

git clone https://github.com/0xzerolight/watchpost.git
cd watchpost
docker compose up -d

Without Docker (Rust 1.88+, no system dependencies beyond a C toolchain - SQLite is compiled in):

git clone https://github.com/0xzerolight/watchpost.git
cd watchpost
cargo run --release

Then open http://127.0.0.1:8080 and paste a token into the setup page. Outside Docker the database lands in ./data/watchpost.db; set WATCHPOST_GITHUB_TOKEN beforehand to skip the setup page entirely. The release profile uses fat LTO and a single codegen unit, so the first build takes a couple of minutes.

Updating
curl -fsSL https://raw.githubusercontent.com/0xzerolight/watchpost/main/scripts/update.sh | bash

or, in the install directory:

docker compose pull && docker compose up -d

The database is backed up before any schema migration the new image performs, and the newest three backups are kept.

Choosing a different port

WATCHPOST_HOST_PORT in the install directory's .env is the port on your machine; the installer writes it. It is deliberately not WATCHPOST_PORT, which is the port the binary binds inside the container and which the image fixes at 8080.

WATCHPOST_HOST_PORT=9000 curl -fsSL https://raw.githubusercontent.com/0xzerolight/watchpost/main/scripts/install.sh | bash
Configuration

All settings are environment variables, read from .env by compose. See .env.example.

Variable Default Meaning
WATCHPOST_GITHUB_TOKEN (unset) Token used for every API call. Optional: with none set, watchpost serves a setup page that saves one to the database instead. When it is set it wins over a saved token, and the settings page offers no way to change it
WATCHPOST_CRON 0 5 * * * * Collection schedule, six fields (seconds first), UTC. An unparseable value falls back to the default. Leave it unset to set the interval on the settings page instead (10m, 6h, 1h 30m; between 5m and 14d), which applies without a restart; setting it here wins over that and removes the field
WATCHPOST_DB_PATH ./data/watchpost.db SQLite file. /app/data/watchpost.db in the image
WATCHPOST_HOST 127.0.0.1 Bind address. The image sets 0.0.0.0
WATCHPOST_PORT 8080 Bind port
WATCHPOST_LOG info tracing filter, e.g. watchpost=debug
WATCHPOST_GITHUB_API_BASE https://api.github.com Override for GitHub Enterprise. Must be http/https; a missing trailing slash is added
WATCHPOST_TZ UTC IANA zone name (e.g. Europe/Madrid) the UI displays times in. An unknown name is a startup error, not a silent fall back to UTC

Two more are read by compose rather than by the binary: WATCHPOST_HOST_PORT (the published host port) and PUID/PGID (the host uid/gid the container drops to, default 1000).

Features

  • Outlives the 14-day window - views, clones, referrers and popular paths sampled hourly and kept forever, long after GitHub has forgotten them
  • Promo event timeline - add a post, a talk or a release announcement and it renders as a marker on every chart for that repo, so spikes have causes
  • Stars, forks, watchers, issues and open PRs sampled daily, with star history backfilled from the stargazers API on the first sync
  • Release asset download counts per tag
  • One SQLite file, one static binary - no database server, no CDN, no JavaScript build step. Outbound traffic goes to the GitHub API and nowhere else
  • Instant period switching - 7, 30, 90, 365 days or all time. The whole history ships with the page, so zooming never hits the server
  • Honest numbers - missing days render as gaps rather than invented zeros, and uniques are never summed across days
More features
  • Sortable all-time referrer and popular-path tables
  • Freeform event kinds with markdown notes, each kind auto-assigned a colour shared by its badge and its chart markers
  • Manual Sync now with a live status banner, plus per-repo error and backoff state on the Settings page
  • Dark mode following your OS, reduced-motion support, and WCAG AA contrast throughout
  • watchpost --doctor prints a secret-safe diagnostic snapshot - config, schema version, row counts, rate-limit budget and per-repo sync state
  • Pre-migration database backups taken through SQLite's own backup API, so they are consistent even mid-write
How It Works
  1. Every hour, and once at startup, watchpost calls the GitHub API for each tracked repository.
  2. Results are written to a local SQLite file, one row per repo per UTC day. Writes are idempotent, so a repeated pass overwrites rather than double-counts.
  3. Traffic days GitHub is about to forget are already stored, so history accumulates past the 14-day window.
  4. Your events are drawn as vertical markers on every chart for that repo, lining spikes up with whatever caused them.

More detail, and the caveats worth knowing before you draw conclusions from the numbers, in ARCHITECTURE.md.

Security

No built-in authentication by design (single-user tool). Anyone who can reach the port gets the whole app, including write access to your events, the sync button and the token form. Both defaults keep it private: WATCHPOST_HOST is 127.0.0.1 and both compose files publish to 127.0.0.1:8080 only. For remote access, put it behind a reverse proxy that does the authenticating (Authelia, Authentik, Caddy basicauth) and forward X-Forwarded-Proto: https. See SECURITY.md.

Troubleshooting

Issue Fix
No repositories listed Open Settings, press Refresh from GitHub, tick the repos you want and Save. Nothing is tracked by default.
Views and clones charts are empty The token is missing Administration: read, or the repo is not one you own or administer. --doctor shows the per-repo last error.
Container exits, or "unable to open database" data/ is not writable by the uid the container runs as. Set PUID/PGID in the install directory's .env to your own uid/gid and restart.
Syncs stop and nothing updates You are rate limited. --doctor prints the remaining budget and the reset time. Collection resumes on its own.
"database was written by a newer build" You downgraded. Reinstall the newer version, or restore one of the data/watchpost.v*.bak files.
Startup fails with a timezone error WATCHPOST_TZ must be an IANA zone name such as Europe/Madrid, not an abbreviation or an offset.
Times look shifted Displayed timestamps follow WATCHPOST_TZ, but day buckets are always UTC - GitHub returns traffic already summed per UTC day and those buckets cannot be re-cut.

Still stuck? Run docker compose exec watchpost watchpost --doctor for a secret-safe diagnostic snapshot - effective config (the token as last-4 and length only, plus where it came from), database path, schema version, row counts, rate-limit budget and per-repo sync state - and paste it into a bug report. Logs: docker compose logs -f.

Contributing

Contributions of any kind are welcome.

Bug reports and feature requests -> Issues. Questions and discussion -> Discussions.

License

GNU Affero General Public License v3.0 or later. See LICENSE.

About

Self-hosted GitHub repo analytics with permanent history and note markers.

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages