Architecture
┌──────────────────────────────────────────────────────────┐
│ Browser (Vue 3 SPA) │ Mobile / third-party (REST) │
└──────────────┬───────────────────────────────────────────┘
│ HTTP
┌──────────────▼───────────────────────────────────────────┐
│ server HTTP adapter · /api/v1 · middleware · ETag │
├───────────────────────────────────────────────────────────┤
│ agent bounded tool loop · sessions · confirmation │
├───────────────────────────────────────────────────────────┤
│ tools single capability source (MCP + agent) │
├───────────────────────────────────────────────────────────┤
│ service use cases · transaction boundaries · tree build │
├───────────────────────────────────────────────────────────┤
│ core domain models · ports (interfaces) · errors │
├────────────┬───────────────┬──────────────────────────────┤
│ store │ index │ textproc │
│ filesystem │ SQLite + FTS5 │ CJK tokenizer / snippets │
└────────────┴───────────────┴──────────────────────────────┘
Principles
- Files are the source of truth. Notes are Markdown files; the database is a disposable, rebuildable index.
- Dependency inversion.
servicedepends only on interfaces declared incore, so backends (e.g. S3 assets) can be swapped without touching business logic. - Thin adapters. The same
servicepowers REST, WebDAV, MCP, the offline CLI and the in-app AI agent; the tool surface lives once intools, andmcpis only a JSON-RPC shell over it. The asset backend is acore.AssetStorebehind aSwitchableAssetStore, so it can be swapped between local storage and S3 at runtime. - One responsive UI. The Vue SPA adapts to phones by breakpoint (drawer sidebar, bottom-sheet panels, touch basics) rather than shipping a separate mobile build.
- Safe writes. Atomic writes (temp → fsync → rename) and optimistic concurrency (content hash / ETag → 409).
- One rendering contract. The editor reading state, public pages and the exported
static site share a single stylesheet (
internal/sitegen/content.css) and the same DOM contract; heavy renderers (highlight.js/KaTeX/Mermaid) are vendored at the app's versions and loaded on demand, so the export renders like the app and works offline.
Packages
Package |
Responsibility |
|---|---|
|
Domain models, ports, sentinel errors |
|
Use cases (notes, search, auth, email/mail, site settings, capture, links, archive, AI, runtime storage config); incremental change feed ( |
|
Filesystem note/asset repository |
|
SQLite + FTS5 index, migrations, change log ( |
|
CJK tokenizer, wiki-links, snippets |
|
Frontmatter parse/serialize |
|
Revision snapshots (with retention), soft delete |
|
fsnotify-based live reindex |
|
Portable ZIP export/import |
|
S3-compatible asset backend |
|
|
|
Single capability source shared by MCP and the AI agent ( |
|
AI agent: bounded tool loop, in-process sessions, two-phase confirmation, audit |
|
MCP server (protocol |
|
Offline command-line adapter (notes, history, trash, assets, static site) |
|
OpenAPI spec (drives |
|
Static documentation-site export: app-aligned DOM ( |
|
OpenAI-compatible chat client (including function/tool calling) |
|
Application wiring and lifecycle shared by the headless and desktop binaries: instance lock, port file, listener, graceful shutdown, external-file import, sync-engine lifecycle, base context cancellation (SSE safe shutdown) |
|
Desktop ↔ server vault sync engine: HTTPS/loopback remote client (manifest, incremental |
|
GitHub Releases version check (report only; never downloads or installs) |
|
Environment config; desktop config ( |
|
HTTP routing/middleware, static handler (gzip + caching), embedded frontend, sync endpoints ( |
Desktop shell
The native app (cmd/overview-desktop) is a Wails v3 shell over the same internal/app
server. It owns only the OS-facing concerns — native window, system tray, single instance,
.md file associations, overview:// deep links, drag-and-drop and launch-at-login — while
every piece of business logic stays in the shared packages. The shell source is guarded by
//go:build windows || darwin || desktop, so a plain CGO_ENABLED=0 go build ./... still
works without GTK headers.
Local-only mode tightens the server instead of forking it: it listens on loopback, defaults
to auth=none and disables MCP/WebDAV, and adds a Host/origin guard (loopback Host plus
same-origin state-changing requests) in place of cookie CSRF protection. The
/api/v1/desktop/* endpoints exist only in local mode — a headless server answers 404.
The lifecycle is shared: both binaries call app.New/Start/Stop, and a data-directory
instance lock plus Wails' single-instance channel keep one server and one window. See
Desktop.
Static serving and 404s
The embedded frontend is served by a custom handler (gzip; content-hashed assets/* cached
immutable, everything else revalidated with a weak content-ETag). A request that misses the
filesystem falls back to index.html only when it is not an asset: assets/* and a fixed
top-level allowlist (icon.svg, the PNG icons, manifest.webmanifest, sw.js, favicon.ico,
robots.txt, sitemap.xml) are the only asset paths, so refreshing a note route such as
/note/<path>.md — which has an extension — still serves the SPA. Missing assets return a
non-HTML 404 instead of being masked by index.html, and an absent bundle returns a
self-contained HTML 404 page. Unknown client routes render a catch-all 404 view in the SPA
(the docs-site render mode redirects them to its home instead).
Vault sync
The desktop shell attaches the sync engine to the shared app through a SyncFactory; the
engine is started and stopped with the app and also implements the server's SyncHooks, so
the same value backs the desktop-only /api/v1/desktop/sync* endpoints (a headless server
registers none of them and answers 404). The server side adds a change counter
(notes.changed_seq) and deletion tombstones (migration 0010), folder tombstones and a
shared folder change sequence (migrations 0011/0012, so empty folders are observable), a
persistent vault id, a strong-ETag GET /api/v1/sync/manifest, a cursor-based incremental
GET /api/v1/sync/changes, an SSE GET /api/v1/sync/events fed by an in-process
service.ChangeBus, faithful raw note writes and path-based asset writes (including S3, via
s3store.Restore). The client reconciles the manifest in SyncOnce (single-flight) and
subscribes to the change stream, debouncing events into SyncOnce while keeping interval
polling as a fallback; it keeps its bookkeeping in <DataDir>/sync.json (with the remote
cursor) and the API token in the separate <DataDir>/.sync-token. See Desktop for the
user-facing behaviour and
docs/DESIGN.md
(ADR-067…083) for the design.
Testing
- Go:
go test ./...— unit tests per package plushttptestintegration tests, includingsync(three-way reconciliation, direction guards, conflict copies, empty-folder sync, cursor persistence, state/token handling), the sync HTTP endpoints (manifest ETag/304, incremental/sync/changes,/sync/events, raw writes, asset path guards), the desktop endpoints (/desktop/vault|restart|open-folder), no-auth owner (auth_none_test), error-code mapping, folderchanged_seq/tombstones, andconfigvault validation/PrepareDataDir. - Frontend:
npm test(Vitest) for pure logic,vue-tsctype-check,vite build. - CI:
.github/workflows/ci.ymlruns the backend race tests, frontend checks andgolangci-linton every push/PR, plus a Windows desktop-shell compile smoke test. Pushing av*tag builds a multi-arch image to GHCR (.github/workflows/docker.yml) and builds the desktop shells for Windows/macOS/Linux (.github/workflows/desktop.yml), publishing them as release assets.