One self-hosted service, your whole GitHub-stats card. A daily cron snapshots your public GitHub stats (languages, stars, forks, followers) into SQLite, and the same service serves:
/languages.svg— an animated profile card: language bar, logo tiles, cumulative lines-of-code and language-share graphs (see it live at/demo)/api/*— a small JSON API over your stats history (/swaggerfor docs)
(the demo card, rendered from deterministic stub data — the real one shows your numbers)
GitHub does not keep historical language/star data — this snapshots it yourself, so history starts accumulating from day one.
cp .env.example .env # 1. set GITHUB_USERNAME (that's the only required var)
docker compose up -d --build # 2. build + run- Point the embed below at your domain. Everything else — handle, theme,
logo, animation — is env config (see
.env.example), no code edits needed.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://your-domain/languages.svg?theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://your-domain/languages.svg?theme=light" />
<img alt="coding stats" src="https://your-domain/languages.svg" />
</picture>Since DEFAULT_THEME=auto, a single <img src=".../languages.svg"> also works:
the SVG carries both palettes and follows the viewer's OS appearance where the
renderer supports it (the <picture> form is the guaranteed-everywhere path).
830px wide, five sections: header with totals → language bar → logo tiles with
percentages → two daily line graphs (cumulative lines of code, language share
over time) → every language ever committed, as tiles. Everything animates in
sequence and collapses to instant rendering under prefers-reduced-motion.
Graphs need history. They're driven by your git history (per day, lines and
commits per language, computed from bare clones kept next to the database). A
fresh install has none, so the card automatically renders without the graphs
and they appear once ≥ 14 days exist. Force with SHOW_GRAPHS=true|false.
There's nothing to schedule — the service does it all itself:
- On boot it takes a snapshot (GitHub API →
snapshotstable) and syncs git history (clones your public repos once, then just fetches →commit_historytable). First launch backfills everything automatically. - Every hour it re-ingests only if the last snapshot is older than
INTERVAL_HOURS(default 24). That's the only knob; failures never crash the server — it keeps serving the last snapshot.
Prefer your system's cron? bun run ingest takes one snapshot and exits —
point crontab at it and skip the built-in scheduler entirely.
Only GITHUB_USERNAME is required — every other knob is optional with a
working default. The full annotated list lives in .env.example:
GITHUB_USERNAME |
required — your GitHub login |
GITHUB_TOKEN |
any PAT, no scopes: 1-request GraphQL snapshots + official colors |
INCLUDE_PRIVATE |
false (default). true leaks private-repo stats into the public SVG — needs a scoped token, think twice |
HANDLE, LOGO, LOGO_SIZE/Y/INSET |
branding |
DEFAULT_THEME, SHOW_BAR, SHOW_GRAPHS, SHOW_ALL_LANGUAGES |
layout (SHOW_GRAPHS=auto by default) |
ANIMATION_PRESET, ANIMATION_SPEED, ANIMATION_<SECTION>_<FIELD> |
motion |
PORT, DB_PATH, INTERVAL_HOURS, CORS_ORIGIN, API_SECRET, REFRESH_SECRET |
service |
DEMO, GITHUB_API_BASE, GITHUB_API_URL |
development |
Query params override per-embed: ?theme=auto|light|dark, ?count=1..10
(languages in the bar; rest grouped as Other).
Never made a token before? No code or account upgrades needed:
-
On GitHub, click your avatar → Settings → Developer settings (bottom of the left sidebar) → Personal access tokens → Fine-grained tokens → Generate new token
-
Give it any name and pick an expiration
-
Under Repository access, select Public Repositories (read-only) — that's all this service needs. Don't grant any extra permissions.
-
Copy the
github_pat_...token and put it in your.env:GITHUB_TOKEN=github_pat_your-token-here
What it unlocks: 1-request GraphQL snapshots, GitHub's official language colors, contribution history, and 5,000 requests/hour instead of 60. Without it, everything still works — just slower and with bundled colors.
src/
index.ts app.ts env.ts service core: boot, wiring, validated config
types.ts provider.ts the data contract + in-process SQLite provider
stats-shape.ts config.ts snapshot→API shape, env→renderer settings
card/ everything that draws the SVG
languages/ linguist datasets: colors, logos, extensions
cron/ snapshotting, git-history sync, JSON API
Your own mark — drop a self-contained SVG at assets/logo.svg and it
replaces the built-in mark (spinning in the top-right corner). It's sanitized
on load (scripts, event handlers and external references are stripped), scaled
to fit and centered — multicolor marks keep their colors. LOGO=dot for a
pulsing dot instead, LOGO=none to remove it, LOGO_SIZE / LOGO_Y /
LOGO_INSET to nudge the position. This one is read at runtime: restart and
you're done, no rebuild.
Language logos — three layers, first match wins:
| Layer | Where | Notes |
|---|---|---|
| 1. Hand-curated | src/languages/logos.ts |
✏️ edit this one — overrides everything |
| 2. Auto-matched | src/languages/auto-logos.ts |
🤖 GENERATED from simple-icons — don't hand-edit |
| 3. Fallback | first letter of the name | automatic |
To add or fix a language logo:
-
Grab the path from simpleicons.org — search the icon, download the SVG, copy the
<path d="...">data (all icons are 24×24; a rare non-square one like Java also needs aLOGO_VIEWBOXentry insrc/card/parts.ts). -
Add it to
src/languages/logos.ts, keyed by the simple-icons slug:"myslug": "M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 ...",
-
If the language doesn't light up at all, its name → slug mapping is missing: add it to
NAME_TO_SLUGinsrc/card/parts.ts(e.g."c#": "csharp").
These are code files, so pick changes up with a rebuild/redeploy.
cp .env.example .env
bun install
bun run dev # server + scheduler, real data
bun test # unit tests
DEMO=true bun run dev # deterministic stub data, zero network — /demo to preview
bun run mock & # fake GitHub API on :9999
GITHUB_API_BASE=http://localhost:9999 GITHUB_API_URL=http://localhost:9999/graphql \
GITHUB_USERNAME=mockuser DB_PATH=/tmp/t.db bun run dev
bun run ingest # one-shot snapshot, then exit (for system-cron users)Any Docker host works; on Coolify: new resource → Docker
Compose → this repo → set GITHUB_USERNAME → attach a persistent volume at
/data (the compose file already mounts ./data) → expose port 3000.
snapshots(taken_at PK, raw_json, languages, total_stars, total_forks, followers, repo_count)
commit_history(day, language, commits, added, deleted) -- PK (day, language)snapshots— one row per day from the GitHub API; same-day re-runs UPSERT. Raw responses kept for future re-analysis.commit_history— aggregated from git history; recomputed on every sync (idempotent, self-healing). Bare clones persist in<db_dir>/stats-clones, so daily syncs only fetch new commits.
Schema migrations run automatically on boot — no manual steps.
GITHUB_TOKEN set |
no token | |
|---|---|---|
| API | GraphQL (1 request/snapshot) | REST (~1 request per repo, chunked) |
| Rate limit | 5,000 req/hr | 60 req/hr — fine for daily snapshots |
| Contribution calendar | included | not available (null) |
| Language colors | from GitHub | bundled linguist color map |
Private repos are always excluded unless you explicitly set
INCLUDE_PRIVATE=true — even a scoped token can't leak them in by accident.
Language identification, colors and logos are powered by the full github/linguist dataset (MIT, © GitHub): extension→language mapping for git-history classification, official colors for ~690 languages, and ~90 auto-matched logo paths from simple-icons (CC0) with hand-curated overrides.
- simple-icons — language logos (CC0)
- Font Awesome — Java coffee cup (CC BY 4.0)
- Lucide — star/fork/person icons (ISC)
- Cascadia Code (SIL OFL 1.1), Quicksand (SIL OFL 1.1)
Code is MIT. Embedded third-party assets (logos, icons, fonts) keep their own licenses — see Credits.