spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
1<!--2 ─────────────────────────────────────────────3 SPB Git — Personal Git Platform4 ─────────────────────────────────────────────5 Author : Simon-Pierre Boucher6 Contact : contact@spboucher.ai7 File : README.md8 Purpose : Project overview, quick start, operations guide9 License : MIT © Simon-Pierre Boucher10 ─────────────────────────────────────────────11-->12<div align="center">1314# SPB Git1516**The personal git platform of Simon-Pierre Boucher**1718[](https://nodejs.org)19[](LICENSE)20[](https://fastify.dev)21[](https://git.spboucher.ai)2223Self-hosted software forge · every repo public & clonable · owner-only writes2425`https://git.spboucher.ai`2627</div>2829---3031## What it is3233SPB Git is a complete, self-hosted GitHub-equivalent built for one person:3435- **Git hosting** — Smart HTTP v2 (streamed `upload-pack`/`receive-pack`), anonymous clone/fetch, PAT-authenticated push, post-receive hooks, zip/tar.gz snapshots.36- **Showcase web UI** — SSR (Nunjucks), dark/light themes, pinned repos, activity feed, contribution heatmap, GitHub-grade README rendering (badges, mermaid, task lists, relative images), shiki-highlighted file browsing with line anchors, commits, diffs, blame, branches, tags, global search, OG cards, Atom feed, sitemap.37- **`spbgit` CLI** — the terminal command center: `init`, `token`, `list`, `create --push`, `clone --all`, `status`, `commit`, `push`, `pull`, `sync`, `open`, `info`, `rm`, `doctor`.38- **JSON API** — public reads, token-gated writes at `/api/v1/*`.3940**Filesystem is the database.** Bare repos under the git root are the source of truth; `meta.json` holds descriptions/topics/pins; everything expensive is cached per `<repo>@<sha>` and busted on push.4142## Quick start (development)4344```bash45npm install # also fetches self-hosted fonts46cp .env.example .env # point the paths at ./dev/* for local work47npm run dev # http://127.0.0.1:742048```4950Mint a first token, then wire up the CLI:5152```bash53node --input-type=module -e "54import { loadConfig, ensureDirs } from './src/config.mjs';55import { TokenStore } from './src/auth/token.mjs';56const c = loadConfig(); ensureDirs(c);57console.log((await new TokenStore(c.dataDir).create('bootstrap')).token);58"59npm link # exposes `spbgit`60spbgit init # paste server URL + token61spbgit create hello --push # first repo, live immediately62```6364## Deployment (node m3u96a)6566```bash67# on m3u96a, from the app directory68bash deploy/setup-m3u96a.sh69```7071The script is idempotent: checks node ≥ 20 / git / ngrok / pm2, creates the72data prefix (`/srv` on Linux, `~/srv` on macOS), installs production deps,73writes `.env`, prints the **bootstrap PAT once**, and starts both pm2 apps74(`spbgit-server`, `spbgit-tunnel`). Run `pm2 startup` once so everything75survives a reboot. DNS: CNAME `git.spboucher.ai` to the ngrok edge target76(see `deploy/ngrok.yml`). Nightly backups: cron `deploy/backup.sh` (keeps 14).7778A systemd unit is provided as the documented alternative: `deploy/spbgit.service`.7980## Scripts8182| Command | Purpose |83|---|---|84| `npm start` / `npm run dev` | run the server (dev = watch + template reload) |85| `npm test` | vitest — unit + full clone/push round-trip e2e |86| `npm run lint` | eslint |87| `npm run check:headers` | fail if any file lacks the author header |88| `npm run inject:headers` | add missing headers in bulk |89| `npm run fetch:fonts` | (re)download self-hosted Inter / JetBrains Mono |9091## Security model9293- All repositories are **public read-only**; there is no private flag anywhere.94- Writes require a PAT (`spbgit_<id>_<secret>`), stored argon2id-hashed, revocable, with last-used tracking.95- Repo names match `^[a-z0-9][a-z0-9._-]{0,63}$`; tree paths are traversal-checked twice (validation + prefix check).96- Rendered Markdown is sanitized with a strict allowlist (badges, `<details>`, `<kbd>`, align HTML survive; scripts, iframes, event handlers never do).97- `/raw` serves with `nosniff` + sandbox CSP, and repo HTML is served as `text/plain`.98- `/internal/*` hook routes accept loopback connections only.99100## License101102MIT © [Simon-Pierre Boucher](mailto:contact@spboucher.ai)103