braid-viewer — desktop app
braid-viewer is a native desktop app built with Tauri v2
that wraps the braid React UI. It lets you open multiple braid project folders,
switch between skeins, and sync directly to your sync server — no CLI or local
server required.
Running / building
# Development (hot-reload; requires tauri-cli and Node.js)
cargo xtask viewer-dev
# Release app (Tauri bundle — .app/.exe/.AppImage). The only command that
# produces a *runnable* standalone app.
cargo xtask viewer-build
Install tauri-cli once:
cargo install tauri-cli --version '^2' --locked
The viewer’s frontend (project chooser, switching, add/remove) has React
component tests under ui/src/*.test.tsx (vitest + Testing Library, with Tauri
and automerge mocked). Run them with:
cargo xtask test-ui # or: cd ui && npm run test
They also run as part of cargo xtask ci and in ci.yml.
Do not run
cargo build --release -p braid-viewerto launch the app. In Tauri v2 a runnable binary requires thecustom-protocolfeature, which only the Tauri CLI (cargo tauri build/cargo xtask viewer-build) sets. A plaincargo buildomits it, so the binary starts in dev mode and tries to load the Vite dev server (http://localhost:5173); with no dev server running you get a blankERR_CONNECTION_REFUSEDwindow.cargo build -p braid-vieweris therefore only a compile smoke check (whatviewer.ymlCI runs) — not a way to produce a usable app.
Logs
The app logs to stdout and to a rotating braid-viewer.log in the platform log
directory:
- Linux:
~/.local/share/org.cscheid.braidviewer/logs/ - macOS:
~/Library/Logs/org.cscheid.braidviewer/ - Windows:
%APPDATA%\org.cscheid.braidviewer\logs\
A startup line records each window’s resolved URL — if it shows
http://localhost:5173 you built a dev binary (see the warning above). Because
the release exe runs with no console on Windows, this file is the primary way to
debug a launch failure. Logs are also forwarded to the webview console
(right-click → Inspect; available in release because the devtools feature
is enabled).
Installing a release bundle
Pre-built desktop bundles (macOS .dmg, Windows installer, Linux
.AppImage/.deb) are attached to each
release. They are currently
unsigned — on macOS, right-click → Open the first time; on Windows, choose
“More info → Run anyway” at the SmartScreen prompt.
Per-OS prerequisites
Linux (Ubuntu / Debian)
sudo apt-get install -y \
libwebkit2gtk-4.1-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev \
libsoup-3.0-dev build-essential
Minimum supported: Ubuntu 22.04 (ships WebKitGTK 4.1).
macOS
Xcode Command Line Tools (xcode-select --install). No extra packages needed.
First launch on macOS may show a quarantine dialog — right-click → Open to bypass, or:
xattr -dr com.apple.quarantine target/release/braid-viewer
Windows
MSVC toolchain + WebView2 runtime (ships with Windows 10 1803+ / Windows 11).
Build with x86_64-pc-windows-msvc.
Adding projects
- Launch
braid-viewer. - Click + Add project and pick a folder that contains a
.braid.tomlwith adoc_id. - The viewer connects to the sync server declared in that folder’s
.braid.toml(orwss://sync.automerge.orgby default).
The viewer remembers the last-opened project across restarts. Project paths
are stored in ~/.config/braid/viewer.toml — paths only, never secrets.
Switching / removing projects
- Switch: click ⇄ Projects in the header to return to the chooser, then pick another registered project (or add one). Switching shuts down the previous skein’s sync connection before opening the next, so only the active project syncs.
- Remove: in the chooser, click the × next to a project to drop it from
the list (
viewer.toml). This only forgets the path — it never touches the folder or its.braid.toml. Removing the project you’re currently in returns you to the chooser.
Offline / warm start
The viewer caches each project’s skein in IndexedDB (via
@automerge/automerge-repo-storage-indexeddb), namespaced by folder path. The
backend pins the webview’s data_directory to the OS app-local data dir
(app_local_data_dir, e.g. %LOCALAPPDATA%\org.cscheid.braidviewer on Windows,
~/.local/share/org.cscheid.braidviewer on Linux) so that cache survives
restarts deterministically. Cached data loads instantly and while the sync
server is unreachable; offline edits merge back on reconnect (automerge is a
CRDT, so this is conflict-free).
Verify per OS: IndexedDB persistence under WebKitGTK (
tauri://scheme on Linux) has been historically unreliable. Confirm warm start on each target — launch, sync, quit, relaunch offline, and check the skein loads without the server.
Relationship to the braid CLI cache
The viewer’s cache is independent of the braid CLI’s. They are separate
automerge replicas of the same document using different storage engines:
- CLI:
samodfilesystem storage atXDG_CACHE_HOME/braid(~/.cache/braid). - Viewer: browser IndexedDB inside the pinned webview
data_directoryabove.
They can’t (and shouldn’t) share a directory — the formats differ. Both sync to the same server, so they converge there; a CLI edit appears in the viewer once both have synced (they don’t talk directly — only through the sync server).
Custom sync servers (allowed_sync_servers)
By default the CSP allows wss://sync.automerge.org. The sync server of every
registered project is also added to the allowlist automatically. To permit a
server before any project on it is registered, add it to
~/.config/braid/viewer.toml:
allowed_sync_servers = ["wss://my-server.example.com"]
The allowlist is built once at startup (registered project servers +
allowed_sync_servers) and applied to the webview’s connect-src. A project
added at runtime on a non-default server therefore needs a viewer restart
before it can sync — or pre-declare the server in allowed_sync_servers.
Architecture
braid-viewer window
└── Webview: ui/ React app
├── isTauri() → true → ViewerShell
│ ├── invoke("list_projects_cmd")
│ ├── invoke("add_project_cmd", { folder })
│ └── invoke("get_config_cmd", { folder }) → docUrl + syncServer
└── automerge-repo (WASM) ↔ wss://sync-server (direct, no Rust relay)
└── IndexedDBStorageAdapter (namespaced per project)
Rust backend (braid-viewer): thin Tauri shell
└── depends on braid-config (lean: no tokio/axum/samod)
├── viewer.rs — project registry (viewer.toml, paths only)
└── ui_config.rs — reads <folder>/.braid.toml, never walks up
Switching projects calls repo.shutdown() on the previous repo before
creating a new one, so only the active skein’s WebSocket stays open.