Bliki Phase 2: Git-Based Filesystem
Phase 2 transformed Bliki from a local static site generator into a git-powered collaborative platform. The working directory became the repository itself — every save is a commit, every webhook triggers a rebuild.
Design Phase
The phase began with extensive design discussions captured in docs/phase2-discussion.md. The core question: how should multiple sources (CLI users, web editors, remote repos) converge on a single working directory?
Key decisions:
- Passive build/serve — the server doesn't push; it pulls. Remote repos and CLI users communicate with the git remote independently. The server operates on whatever is checked out locally.
- User-merged conflicts — the server doesn't resolve merge conflicts automatically. Users pull and resolve conflicts manually on their machines.
- File locking — TTL-based locks prevent concurrent edits from the web editor.
- Webhook-triggered sync — incoming pushes trigger a fetch + reset + rebuild cycle.
The design settled on four interacting components: go-git operations, a webhook handler, file locking, and a watch-rebuild loop.
go-git Integration
github.com/go-git/go-git/v5 became the foundation for all git operations. The internal/git/ package provides:
- repo.go — open, init, clone, fetch, push, reset, commit, add
- lock.go — acquire, release, TTL check (10 minutes default)
- store.go —
ReadFileandWriteFilethat auto-commit on write - webhook.go — HTTP handler for POST
/api/receive
SSH key authentication was a significant addition. The system detects whether a remote uses SSH or HTTPS, persists the auth type in .bliki/auth-type, and uses SSH key files for authentication. This enables --git-remote-url ssh://git@... workflows.
Consolidation: The Build Command
The most visible change was command consolidation. Phase 1 had separate clone and serve commands. Phase 2 merged them into the single build command with flags:
bliki build --git-remote-url <url> # clone + build
bliki build --serve # build + start server
bliki build --git-remote-url <url> --serve --watch # all-in-one
This eliminated the CLI/serve disconnect — one command handles everything. The --git-remote-url flag triggers cloning with optional scaffolding of default files.
Design decision: Removed pull, push, and sync commands. The CLI and server communicate with the remote independently through git. No direct CLI-to-server communication is needed.
Config Centralization
Phase 2 introduced a shared viper instance across all packages. Previously, each command created its own viper instance, leading to configuration mismatches.
The centralized approach means:
--configflag sets the config file path globallyBLIKI_*environment variables override config file values- All packages read from the same viper singleton
- Config keys use kebab-case in TOML, camelCase in Go structs
CLI flag refactoring renamed flags for consistency:
--token→--git-tokenGIT_TOKEN→BLIKI_GIT_TOKEN- Added
--addressfor network binding (default0.0.0.0)
Webhook Handler
The webhook endpoint (POST /api/receive) receives push events from git hosting services:
- Validate HMAC-SHA256 signature using
webhook_secretconfig - Fetch latest changes from remote via go-git
- Reset working directory to
origin/<git_default_branch> - Rebuild if files changed
The webhook supports Forgejo's HMAC-SHA256 signature format. A configurable webhook_secret (env: BLIKI_WEBHOOK_SECRET) authenticates incoming requests.
Config-driven default branch means the reset target is git_default_branch from bliki.toml (default: main), not hardcoded to main.
Watch Mode
The --watch flag enables file watching with auto-rebuild:
- Uses
github.com/fsnotify/fsnotifyfor cross-platform file system events - Recursively watches
content/,theme/, andbliki.toml - Debounces rapid events inline (no separate event channel)
--watch-ignore-filesflag skips backup files (e.g.,*.swp,*.bak)
An earlier implementation had a bug where the event channel was shadowed, causing rebuilds to not trigger. The fix simplified the architecture: a single loop with inline debounce instead of a channel-based event queue.
Docker & CI
Phase 2 added production deployment support:
- Multi-stage Dockerfile — builds the Go binary in a builder stage, ships a minimal runtime image
- CLI command —
bliki build --git-remote-url <url> --serveas the default Docker entrypoint - Makefile — targets for local build, run, and container testing with
.envfile support - Forgejo CI/CD — workflow that triggers on pushes, builds, and pushes Docker images
Known Limitations
Phase 2 left a few design challenges for future iterations:
- Merge conflicts are resolved manually by the user on their machine. The server doesn't attempt auto-merging.
- File locks are acquired at Save time, not at Edit time — there's a gap where another user can claim the lock between Edit click and Save.
- No push from server — the server can push (web editor saves push to remote), but the CLI doesn't push to the server. Both communicate with the remote independently.
What's Next
Phase 2 delivered a git-native workflow: clone a remote, edit content via the web or CLI, and have everything synchronize through git. Phase 3 added user authentication and the inline web editor with real-time rebuild notifications.