Desktop app
Overview also ships as a native desktop application. The same Go binary that powers the server opens a native window instead of (or in addition to) serving the browser UI.
The shell is built with Wails v3 (v3.0.0-beta.26): a small Go
process hosts a WebView pointing at the local Overview server. All application behaviour —
files, index, search, AI, everything — lives in the shared packages, so the desktop app and
the headless server are the same program with a different front end.
What the shell adds
- Native window — an embedded WebView (WebView2 on Windows, WKWebView on macOS,
WebKitGTK on Linux) pointed at
http://127.0.0.1:<port> - System tray — open the window or quit from the tray
- Single instance — launching again raises the existing window; a file or deep link passed to the second launch is forwarded to the first
- File association — opening a
.mdfile imports/navigates to it - Deep links —
overview://note/<path>andoverview://open?path=<path> - Drag and drop — drop Markdown files onto the window
- Launch at login — available as a toggle under Settings → Desktop
- Update notification — checks GitHub Releases and links to the download page; it never downloads or installs silently
- First-run onboarding — choose the default vault, open an existing one (a folder with
notes/oroverview.db), or create a new one; the choice is remembered in<UserConfigDir>/Overview/desktop.json - Vault management — under Settings → Desktop you can change the data directory (native picker), open it in the system file manager, and restart the app to apply a change
- Upgrades load the new frontend automatically — the window opens at
/?desktop=1&v=<version>, so every release is a fresh WebView2 cache entry and there is no stale bundle to clear after an upgrade (the static handler also hashes file contents for itsETag, and missing assets return404instead of an HTML fallback) - Focus mode is escapable — focus mode hides the top bar and sidebar, and a low-key
floating Exit focus button in the bottom-right corner is the visible way out (Esc and
F9also work)
Vault sync (desktop ↔ server)
The desktop app can keep its local vault in step with a self-hosted Overview server, using the same client ↔ server model as Trilium. Open Settings → Sync (desktop only) and enter the server URL and an API token; the status card, conflict list and Sync now button live there too.
- Identity by note
id— frontmatterid(ULID) is the stable cross-device identity; paths are just locations, so moves and renames don't create duplicates. Notes without anidget one on the first sync. - Manifest + strong ETag — the server exposes
GET /api/v1/sync/manifest(vaultId, every note with its content version, and every folder including empty ones) behind a strongETag, and answers304when nothing changed. - Three-way reconciliation — the engine compares the remote manifest with the local vault
and its last-synced state, then pulls, pushes, deletes and moves notes and attachments.
Writes go through a faithful raw channel (
PUT /api/v1/notewithraw: true) that stores the exact Markdown bytes and carries abaseVersion, so a concurrent edit is never silently overwritten. - Direction —
both(default),pull(server → desktop) orpush(desktop → server). - Conflicts are kept, not merged — if both sides edit the same note, the local copy is
saved as a sibling
<name> (conflict-<device>-<ts>).mdand the remote original stays put. Resolve each conflict from the settings page with keep local or keep remote. - Attachments — synced by vault-relative path; pushes are de-duplicated by content hash
and pulls only fill in missing files. S3 vendors now support path-based writes too
(
s3store.Restore), so attachments sync against object-storage backends as well. - Real-time + incremental — the server streams changes over Server-Sent Events at
GET /api/v1/sync/eventsand offers a cursor-basedGET /api/v1/sync/changes(sincenote sequence +sinceTstombstone time). The client subscribes to the stream and reconciles on change (debounced by 500 ms), persisting its cursor; interval polling (default 60s) and the file watcher remain as fallbacks, and a change event is only a hint — the manifest comparison still guarantees convergence. - Empty folders — folder creations, moves and deletions share the note change sequence
(migrations
0011/0012), so a folder that holds no notes syncs as well: parents are created before children and deleted after them. - Oversized attachments — an attachment above the server's upload limit is skipped with a warning in the sync status instead of failing the whole sync.
The token is stored separately in <DataDir>/.sync-token (never in the exportable
sync.json or logs), and plain HTTP is only accepted for loopback hosts — a remote server
must use HTTPS.
Local-first defaults
On the desktop the app is a single-user, local-only tool:
- listens on
127.0.0.1only - authentication is off
- MCP and WebDAV are off
- the data directory is per-user
Because authentication may be off, write requests are protected by a Host and origin
guard (the Host header must be a loopback name, and state-changing browser requests must be
same-origin) rather than cookie CSRF protection. With authentication off, every request is
treated as the owner (admin), so all admin settings — mail, site, object storage, data,
users and API tokens — are reachable without a login. The desktop-only API endpoints
(/api/v1/desktop/*) are not even registered by a headless server — it answers 404.
Setting OVERVIEW_DESKTOP=true switches on these defaults; every one of them can be
overridden with the usual environment variables (OVERVIEW_ADDR, OVERVIEW_AUTH,
OVERVIEW_ENABLE_MCP, OVERVIEW_ENABLE_DAV, OVERVIEW_DATA_DIR).
Data directory
Platform |
Default |
|---|---|
Windows |
|
macOS |
|
Linux |
|
If the directory is missing or empty, the first launch runs an onboarding flow that lets you
use the default, open an existing vault, or create a new one (cancelling keeps the default).
The resolved path is stored in <UserConfigDir>/Overview/desktop.json, so later launches skip
onboarding; you can change it under Settings → Desktop (the change applies after a
restart, which the settings page can trigger for you).
Building
The frontend is bundled into the binary, so build the web app first:
make build-desktop # -> bin/overview-desktop (host platform)
make package-desktop # + tar.gz archive
Or build the shell directly:
Platform |
Command |
Requirements |
|---|---|---|
Windows |
|
WebView2 runtime (no CGO) |
macOS |
|
CGO + Xcode command line tools |
Linux |
|
CGO + GTK4 + WebKitGTK 6.0 |
Linux (older) |
|
CGO + GTK3 + WebKit2GTK 4.1 |
Windows builds are produced with the GUI PE subsystem (-ldflags "-H=windowsgui"), so
launching the .exe does not open a console window. Because a GUI process has no stdout,
the desktop shell logs to a file by default: <DataDir>/logs/desktop.log (set
OVERVIEW_LOG_FILE to override). The logger writes through a fault-tolerant fanout, so one
unavailable destination (such as stdout under the GUI subsystem) never stops the file log.
The shell source is guarded by the build tag windows || darwin || desktop, so a plain
CGO_ENABLED=0 go build ./... on a machine without GTK headers still works for the backend.
Tagged releases build the Windows/macOS/Linux shells in CI
(.github/workflows/desktop.yml) and attach them to the GitHub Release.
Current limitations
- The macOS and Linux builds are compiled and archived in CI but have not been verified on real hardware.
- There are no installers yet (Windows NSIS, macOS DMG, Linux AppImage/
.desktop); the release assets are plain binaries in a.zip/.tar.gz. - The
wails3packaging pipeline is not wired into the repo build; the shell is built with a plaingo build(seebuild/config.ymlfor the metadata that the packager would use). - Updates are only notified — there is no silent auto-update.
- If the preferred port is taken, the app falls back to a random loopback port (written to
.overview-port); the port is not guaranteed to be stable. - Sync is event-driven (SSE) with interval polling as a fallback, but a server is still a single shared vault with no per-user ACL, the SSE bus is in-process (no fan-out across multiple server instances), and oversized attachments are skipped (no chunked upload).
- The desktop config (
desktop.json) lives in the user config directory; a lost sync cursor falls back to one full manifest comparison.
See Architecture for how the shell fits into the codebase, and
docs/DESIGN.md
(ADR-059…086) for the design decisions.