Beads (Steve Yegge) vs WikiHub — Source-of-Truth Comparison
Date: 2026-04-10
Status: Decided
Applies to: WikiHub, ListHub (informational)
Context
Both Beads and WikiHub use the same three-layer pattern — a fast queryable store, a text-serialized git-trackable export, and git hooks/plumbing to keep them in sync. But they flip the source-of-truth direction. Understanding why helps explain WikiHub's architecture to anyone coming from the Beads world (which includes us — ListHub uses Beads for issue tracking).
The shared pattern
- A fast queryable store (SQLite / Postgres)
- A text-serialized git-trackable export (JSONL / markdown files)
- Git hooks or plumbing to keep them in sync
The inversion
| Beads | WikiHub | |
|---|---|---|
| Source of truth | SQLite | Git bare repos |
| Derived/export | JSONL (for git tracking) | Postgres (metadata + search index) |
| Why this direction | Issue tracker fields are structured data — queries need to be fast, nobody hand-edits JSONL | Wiki pages are authored markdown — users expect to git clone, edit offline, push back |
| Bidirectionality | Fully symmetric — either layer can rebuild the other | Asymmetric by design — private content lives only in Postgres, never enters git |
| Content duplication | Yes — same data in both SQLite and JSONL | No — public content in git only, private content in Postgres only, never both |
Why WikiHub chose git-as-truth
The whole thesis of WikiHub vs Notion/Google Docs is that the wiki IS a git repo. Real commits, real history, real git clone. If Postgres were the truth and git just an export, you'd have a slightly fancier ListHub — the git repos would be ZIP downloads with extra steps.
Beads can afford SQLite-as-truth because nobody cares about the git history of an issue tracker. The JSONL export is for portability, not for humans to git log. WikiHub users — Karpathy-wiki people, Obsidian vault owners — absolutely care about git log, git blame, and "I can clone this and read it offline."
WikiHub's no-duplication model
WikiHub already solved the "should we store content in both places?" question:
- Public pages: content in git ONLY. Postgres has metadata/search index. Reads via
git cat-file blob. - Private pages: content in Postgres ONLY (
private_contentcolumn). Never enters git. - Visibility change moves content between stores (not copies it).
This means Postgres is fully rebuildable from git for public content (wikihub reindex --all), but private content + social graph are non-rebuildable — they live only in Postgres. Both backups required.
What WikiHub steals from Beads
.wikihub/events.jsonl— JSONL-as-git-sync-layer for audit events (ACL changes, visibility flips, forks). Append-only, line-diffable, merge-friendly. The single most useful pattern from Beads.- Line-oriented formats under
.wikihub/— merge-friendly, git-diffable. - Hash-based IDs — already aligned via nanoid.
- Daemon pattern — for concurrent CLI access (Beads has
auto-start-daemon: trueto avoid SQLite lock contention).
Open question: defer git push → Postgres sync to v2?
If nobody is pushing via git push in early days (everyone uses web editor), we could:
1. Keep Postgres → git (web writes sync to repo) ✅
2. Defer git → Postgres (post-receive hook path) to v2
3. git push returns an error: "Push via web editor for now"
This cuts half the sync complexity while keeping the "git is real" story for readers/cloners. Add the push path when someone actually needs it.
Consequences
- WikiHub's write path is slightly more complex than Beads (git plumbing on every web edit)
- WikiHub's read path for public content hits git (~3-5ms per
git cat-file), not a DB query - WikiHub needs BOTH git backups AND Postgres dumps (asymmetric non-rebuildability)
- The no-duplication model means no sync bugs for content — it's only in one place
Sources
- Steve Yegge — Beads
- Introducing Beads — Steve Yegge on Medium
- WikiHub codebase:
~/code/wikihub(app/git_sync.py,app/models.py,hooks/post-receive) - Fork C session (2026-04-08) resolved the "Yegge model" open question to Beads