## Product Thesis Monoplan is a low-friction, FOSS, single-human-user/small-team, E2EE, multi-device capture/clarify/organise tool for ideas, intents, goals, projects. It is flexible but built with particular regard to improving users' productivity & focus. Workflow: a reserved primary capture list ("Inbox", id `inbox`), any number of user-created lists (each viewable as a fixed Backlog/Live/Done board), and a bin. Items move between lists, transition through the lifecycle (Backlog, Live, Done, Binned), can be restored, and deleted. ## Architecture - **Rust core** (`core/`) — Loro CRDT, E2EE, `Replica` (the per-doc sans-IO sync state machine). Compiles to native (CLI, server) and WASM (web) via `core/web/`. - **Rust server** (`server/`) — sqlite-backed, sequenced encrypted-blob store + auth + WS relay. The server is *dumb*: it cannot read op contents, cannot run a Loro doc, cannot validate semantics. Its job is auth, ordering, durability, frontier tracking, snapshot orchestration. TODO: Compaction may put an asterisk on "DUMB". - **CLI** (`cli/`) — Monoplan CLI - **Web** (`js/web/`, consuming `core/web/` wasm via `js/core/`) — browser client; multi-device proof spans CLI ↔ web. - **iOS** (`apple/MonoplanIOS/`) — native SwiftUI client over the same embedded engine and FFI, no webview; first surface (sign in / out, sync state) built 2026-11-05, see `apple/README.md`. **Android** — future E2EE: password-derived KEK wraps a randomly-generated DEK. DEK encrypts every update blob. Server has no key. Recovery via a user-held recovery code (independent wrap of DEK) is implemented; server-assisted escrow (Vault-backed, opt-in) is future work. Sync: WebSocket per device. Auth on upgrade. Updates are append-only encrypted blobs with server-assigned monotonic seqs. Each device tracks its `last_acked_seq`; the minimum across active devices is the compaction horizon. Snapshots are produced by the most-acked active client on server request. Migrations: while pre-release, keep exactly one migration file per database (`001_init.sql`) and edit it in place — never add incremental or legacy-bridge migrations. ## Source of truth - `spec/*.md` — the contract for what's being built. **Before implementing anything, read the relevant spec.** - `architecture.md`, `auth.md`, `cli.md`, `data-model.md`, `encryption.md`, `storage.md`, `sync-protocol.md`, `testing.md` ## Specs | File | Concern | |---|---| | [`spec/architecture.md`](spec/architecture.md) | Workspace layout, crate graph, server-is-dumb thesis | | [`spec/encryption.md`](spec/encryption.md) | DEK / KEK / recovery code, wrap-states, password change | | [`spec/crypto.md`](spec/crypto.md) | Cryptographic primitive inventory; companion to `encryption.md` | | [`spec/auth.md`](spec/auth.md) | HTTP signup / login / recover, device register / revoke, token model | | [`spec/admin.md`](spec/admin.md) | Operator-only JSON API, authentication, deployment exposure | | [`spec/sync-protocol.md`](spec/sync-protocol.md) | WS framing, push / pull / ack, frontier, snapshot orchestration | | [`spec/storage.md`](spec/storage.md) | Sqlite schema, indexes, compaction policy | | [`spec/data-model.md`](spec/data-model.md) | Loro doc layout, Item / ListMeta, lifecycle semantics | | [`spec/vv-wal-separation.md`](spec/vv-wal-separation.md) | Local WAL (crash recovery) vs VV-derived sync separation; push_id retry contract | | [`spec/trim-minimal.md`](spec/trim-minimal.md) | Plan (not built), read first: trim at the newest chain row under the horizon, producer validates by loading its own blob, epoch on push, devices self-trim via Loro-import-as-oracle, unsafe devices surface stranded changes then wipe and bootstrap | | [`spec/trim-plan.md`](spec/trim-plan.md) | Reference for trimming: verified Loro 1.16 facts with probe code (VV never shrinks, pre-root ops hard-reject, trimmed-over-stale pends, non-cut root exports unloadable), rejected designs, why the seq-based compatibility rule was unsound | | [`spec/peer-id-plan.md`](spec/peer-id-plan.md) | Stable per-device Loro peer id: flock-leased slot pool, VV-width bound | | [`spec/board.md`](spec/board.md) | Board view: fixed Backlog / Live / Done lanes driven by item lifecycle | | [`spec/focus.md`](spec/focus.md) | Focus lens: curated single-tier list-by-reference, FocusRef grammar, auto-remove-on-Done | | [`spec/calendar-plan.md`](spec/calendar-plan.md) | Plan (not built): `when` date/datetime register beside `deadline`, agenda + month grid lenses, floating-only with reserved zone suffix, no recurrence | | [`spec/events-plan.md`](spec/events-plan.md) | Events (built): reserved `events` list whose items surface only on the calendar, ticking optional, Unscheduled section, no item kind; recurrence unsolved | | [`spec/place-plan.md`](spec/place-plan.md) | Place (built 2026-10-07): optional atomic `place` register `{label, lat?, lon?, address?, ref?}` shared by events and tasks; web lookup via Komoot's public Photon, debounced on input (not E2EE); known-place reuse (2026-10-08, account-scoped across every open doc since 2026-11-10): core `place_suggestions`, CLI label resolution, two-tier web field; notifications deferred | | [`spec/search.md`](spec/search.md) | Local search index + command palette query contract | | [`spec/urls.md`](spec/urls.md) | Fragment URLs for items / lists / views, history rules, internal links | | [`spec/cli.md`](spec/cli.md) | Commands, local key storage, device bootstrap UX | | [`spec/sharing-plan.md`](spec/sharing-plan.md) | Multi-doc substrate built (2026-10-19: N open docs per connection, `POST /api/docs`, host-owned handshake, js router); invites built 2026-10-21 (one fragment secret → HKDF lookup id + wrap key, single use, owner-only, never Home; web sharing dialog + invite accept, CLI `share`, member list); member removal + `DocClosed` built 2026-10-26, unilateral owner delete 2026-10-26; cross-doc move built 2026-11-11 (id preserved, adopt then release, linked undo, confirm when the audience changes; core + web only); leave built 2026-11-11 (self-removal on the member route, web "Leave workspace" for non-owners); transfer not built | | [`spec/notes-plan.md`](spec/notes-plan.md) | Plan (not built): `notes` / `text` as mergeable `LoroText`, wasm delta bridge, flat rich text, images by reference | | [`spec/subtasks-plan.md`](spec/subtasks-plan.md) | Exploration (not built): subtasks as items located under a parent item, lifecycle cascade, "which part is next" rule | | [`spec/daemon-plan.md`](spec/daemon-plan.md) | Daemon (built 2026-10-21): one binary, two roles; `monoplan daemon` owns sqlite / doc / DEK / WS and serves a msgpack Unix socket; every doc command is a thin client that auto-starts it; one-shot mode and `-s` removed; `daemon.lock` flock is the election. `daemon install|uninstall` and the `Watch` projection tier built 2026-10-26/27; on macOS the app runs the engine in-process via `monoplan-ffi::Engine` (2026-10-27). Not built: locked state, wake hooks. Mirror tier lives on the FFI, see `mac-window-plan.md`. Built behaviour is in `spec/cli.md` "Process model" | | [`spec/mac-window-plan.md`](spec/mac-window-plan.md) | macOS window (decided 2026-10-29): a webview running `js/web` over a **mirror** doc with its own peer, seeded and converged by the in-process `Engine` over the FFI (`subscribe` / `import`, Loro bytes, no DEK / storage / socket in the webview); socket stays CLI-only. Built: phase 1 tray over the FFI (2026-10-29), phase 2 engine side of the mirror (2026-11-03: `HostCmd`, `Engine.subscribe` / `Engine.import`, `Doc::import_mirror_update`), phase 3 the mirror in wasm and JS (2026-11-04: `Replica.mirror`, `Doc.loadWithPeer`, `MirrorBridge` in `js/core`, `createMirrorRuntime` + `MirrorApp` + the `mirror.html` entry in `js/web`, `Session.mirror`), phase 4 the window (2026-11-04: `WebWindow.swift` with the message handler, the `monoplan://app` scheme handler serving the Vite build and proxying `/api` with the device token, hand-off on close / quit, `Engine.session` / `Engine.device_token`, DEK-touching flows hidden under `Session.mirror`; tray opens it). View-model and native SwiftUI rejected until the UI settles | | [`spec/testing.md`](spec/testing.md) | Integration test pattern, CLI driver | | [`spec/known-bugs.md`](spec/known-bugs.md) | Reproduced defects left unfixed: no-op call in a web action batch over-counts undo steps | ## Build & run Root `package.json` is a Bun workspace (`js/*`) with thin script wrappers — there is no JS to build, the scripts just front cargo and the config generator: - `bun run config` — render `local/server.toml` from `js/config/templates/` (see `js/config/README.md`); `local/` holds gitignored dev artifacts - `bun run server` / `bun run cli` — `cargo run -p monoplan-server --` / `cargo run -p monoplan --` (debug build; faster compile, slower sync on real-sized docs). Pass flags after the script name (e.g. `bun run server -- --bind 0.0.0.0:8000`). `bun run cli:prod` for the release build when you need real sync perf. `auth signup` / `auth login` / `auth recover` default to `https://alpha.monoplan.app`; set `MONOPLAN_SERVER=http://127.0.0.1:8000` (or `--server`) for the local server. - `bun run build` / `bun run test` / `bun run fmt` / `bun run lint` — cargo equivalents - `bun run build:wasm` — `wasm-pack build core/`. Always use this from the workspace root; bare `cargo build --target wasm32-...` will try to build `server`/`cli` for wasm and fail. - `bun run typecheck` — `tsc --noEmit -p js/config` ## Git workflow - **Do not use worktrees when the working tree is clean.** Work directly in the checkout. A worktree is only for when the checkout has uncommitted work that must not be disturbed. Worktrees branch from a remote head, which is a stale base whenever local `main` is ahead. - Remotes: `origin` is GitHub (`monoplandev/monoplan`, the up-to-date one); `yokoso` is the self-hosted mirror. Base branches on `origin/main` or local `main`, never on `yokoso`.