Local development
msgbrowse is deliberately easy to build: the core is a pure-Go binary with no C toolchain, no cgo, and no Node anywhere in the runtime path. This page covers the full contributor loop — building, testing, the CSS pipeline, the desktop shell, and the docs site itself.
Toolchain requirements
- Go 1.25 or newer — the only hard requirement for the core.
go.moddeclaresgo 1.25.0and CI builds with Go 1.25. - No C toolchain. The SQLite driver is pure Go
(
modernc.org/sqlitewith FTS5 built in — ADR-0013), so the root module builds withCGO_ENABLED=0. The Makefile exports that pin and CI enforces it, so the binary provably stays cgo-free. - No Node for the app. Node (≥ 20) is needed only if you work on the docs site. The web UI's CSS is built by a standalone binary — see the CSS pipeline below.
- The only cgo in the repository is the desktop shell, quarantined in its own nested Go module.
Clone, build, test
git clone https://github.com/joestump/msgbrowse.git
cd msgbrowse
make build # build ./bin/msgbrowse (pure Go, CGO_ENABLED=0)
make test # go test ./...
make check # gofmt + go vet + tests — the exact CI gate
make check is what CI runs
(ci.yml),
so a green make check locally means the core gate passes. Other useful
targets: make cover (coverage summary), make fmt (gofmt the tree),
make install (go install into $GOBIN/$GOPATH/bin).
Run the server against the synthetic fixtures
The repo ships small, hand-written synthetic archives under testdata/ —
a signal-export style archive (testdata/archive, with its export/ tree and
journal) and an imessage-exporter style one (testdata/imessage). They are
enough to click around the UI:
make build
./bin/msgbrowse --data-dir ./data \
--archive-root testdata/archive \
--imessage-archive-root testdata/imessage \
import
./bin/msgbrowse --data-dir ./data serve # auto-opens http://127.0.0.1:8787
make run is the shortcut for build-then-serve (using whatever roots your
config/env provide). The data dir is the only thing msgbrowse writes;
./data/ is gitignored.
The testdata/ fixtures are synthetic by design. Never import, copy, or
commit a real exported archive (yours or anyone else's) inside the repo
checkout — point --archive-root and friends at a directory outside it. The
archive roots are treated as strictly read-only either way (see the
security model).
The CSS pipeline (no Node)
The web UI uses Tailwind CSS v4 + daisyUI with the bespoke slate themes
(ADR-0007,
ADR-0012). The built
internal/web/static/app.css is committed and served via go:embed, so
the runtime needs no toolchain, no CDN, and the strict CSP holds.
When you change classes in templates, rebuild it:
make css
That downloads the Tailwind v4 standalone CLI (a single binary — no npm,
no node_modules) and the daisyUI package into the gitignored .tools/
directory, versions pinned in the Makefile, then rebuilds app.css from
internal/web/tailwind/input.css.
The drift guard. CI
(css.yml,
job "app.css is up to date") runs make css from a fresh download and fails
the PR if the committed app.css differs from the rebuild. Two gotchas will
trip you here:
-
Stale
.tools/cache. A rebuild against an old cached toolchain can differ from CI's clean download even when consecutive local builds look deterministic. Before committing a CSS change, always rebuild from a clean cache:rm -rf .tools && make css -
Tailwind scans
.gofiles too. Tailwind v4's automatic content detection walks every non-gitignored text file from the repo root — including Go source — in addition to the explicit@source "../templates"ininput.css. Class-shaped strings in.gofiles therefore end up compiled intoapp.css(and removing them changes it), so even a pure-Go change can require amake cssrebuild. Classes composed at runtime in Go (never appearing literally anywhere) must be safelisted via@source inline(...)ininput.css; the docs site is excluded from the scan with@source not "../../../docs-site".
Desktop shell development
The desktop app (ADR-0017) is the
repository's only cgo code, quarantined in a nested Go module
(cmd/msgbrowse-desktop/go.mod, with a replace back to the root) behind the
desktop build tag — so the core stays CGO_ENABLED=0 and the desktop
targets are never prerequisites of any core target.
make desktop-test # the module's headless tests — pure Go, no webview toolchain needed
make desktop-linux # build the Linux desktop app (cgo) to cmd/msgbrowse-desktop/build/bin/msgbrowse
make desktop-linux links the system webview, so it needs the GTK3/WebKit2GTK
dev headers:
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config
The default tags target webkit2gtk-4.1 (Ubuntu 24.04+); on distros still
shipping webkit2gtk-4.0 build with
make desktop-linux DESKTOP_TAGS=desktop,production.
macOS .app builds happen in CI, not locally: the
desktop.yml
matrix builds the darwin/universal .app (including the bundled exporter
toolchain and Syncthing) on v* tags, manual dispatch, and PRs that touch the
desktop shell. You only need the Wails CLI for local shell development on a
Mac — install the pin that matches the wails/v2 library version in the
module's go.mod:
go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
The signing story for those CI-built .app bundles is its own runbook:
macOS signing & notarization.
Docs site development
The documentation site (this site) is a Docusaurus app in docs-site/ — the
one place Node (≥ 20) is required:
cd docs-site
npm install
npm start # rebuild generated content + dev server
npm run build # full production build (fails on broken links)
Hand-authored pages live under docs-site/docs/<section>/; the ADR and spec
sections are generated from the repo sources on every build — never edit
docs-generated/. See
docs-site/CONTENT-PLAN.md
for the ground rules (verify every command against the code before writing
it).
ADRs, specs, and pull requests
- Architecture Decision Records live in
docs/adr/(MADR format) and specifications indocs/openspec/specs/(each aspec.md+design.mdpair). Both render here under Architecture. - PRs should keep
make checkgreen, add tests for new ingest/search/MCP behavior, and — when templates or classes change — commit a freshrm -rf .tools && make cssrebuild so the drift guard passes.