Design decisions
Each record names a choice, the alternatives it beat, and what it costs. They are the same files as docs/adr/ in the repository, published as written.
- Stack and tooling
TypeScript end to end, ESM only, latest stable everything, no backward-compatibility layers: Node 26 (built-in node:sqlite), TypeScript 7 (native compiler), pnpm 12 workspaces, Vite 8, Vitest 5,…
- SQLite is the source of truth; markdown files are a lossless mirror
The graph lives in SQLite: an append-only op log plus state tables (page, block, blockprop, derived ref, blockfts, embedding). The same schema runs on the server (node:sqlite) and in the browser…
- Sync = op log + hybrid logical clocks + per-field last-writer-wins, server validates the tree
Every write anywhere (editor, HTTP API, MCP tool, markdown import) is a list of ops: page.create|rename|prop|delete, block.create|place|text|prop|delete. An op targets one entity and one…
- Short time-ordered ids and fractional-index ordering
Pages, blocks, and property definitions get 14-character ids in lowercase Crockford base32: 45 bits of milliseconds since the epoch (time-ordered, sortable, good for a thousand years) followed by…
- Client packaging: PWA first, Capacitor for stores, Tauri for desktop
One Vite-built web client behind a small platform adapter (storage driver, keyboard insets, haptics, share, files, deep links, lifecycle). - v1 ships as an installable PWA (service worker,…
- Editor: rendered blocks plus one re-parented CodeMirror 6 surface
All blocks render as HTML from our own inline markdown tokenizer. Exactly one editing surface exists at a time: a single CodeMirror 6 EditorView re-parented into the block being edited. The…
- Plugins: one package, optional server and client halves, trusted ESM in v1
A plugin is a directory with a nooklet manifest in package.json (id, API version, optional server and client entries, JSON-schema settings, permissions, declared contributions), or a single…
- One operation registry for HTTP, MCP, and the typed client
Stack: Hono 4 in one process (web client, /api/v1, /mcp, /openapi.json, /sync, /assets), Zod 4 schemas with generated JSON Schema, MCP SDK v2 (@modelcontextprotocol/server +…
- Every operation is a command; keybindings are user data
A command is { id, title, description, category, when, defaultKeys, run }. Core registers every user-facing operation; plugins register theirs, declaratively in the manifest so they appear before…
- Embeddings via Ollama, vectors in sqlite-vec, hybrid search by rank fusion
Server-only embeddings. Provider interface with an Ollama /api/embed implementation (default) and an OpenAI-compatible one; the model is a runtime setting, dimensions are discovered from the…
- Scheduling, repeats, and query blocks use one property/fence syntax, not org-mode
Scheduled and deadline dates are ordinary typed properties, not org-mode drawer lines: scheduled:: 2026-09-12, deadline:: 2026-09-14 14:00 (ISO date, optional time, no weekday, no angle brackets).…
- The importer targets the Logseq file graph only, not the DB version's markdown export
too, from their mirror plus db.sqlite.
- AI parity is a first-class requirement — undo and assets in MVP, live UI control designed for M2
M2, not yet an implementation decision.
- Stay on Node for the server; Bun is a viable later swap, not a now decision
The server keeps targeting Node (26.x, per ADR 001). Bun was evaluated and rejected for now, not on principle — the SqlDriver abstraction in packages/core (ADR 001/003) means the runtime is…
- Live UI control — a dedicated `/ui/live` socket, the existing command registry, consent by default-asymmetry
Follows from ADR 013's forward-looking item. Full survey and rationale: docs/research/09-live-ui-control.md.
- Desktop app — Tauri, pointed at the local server
The desktop app is a Tauri 2 shell. Electron is not used. - v1 loads http://127.0.0.1:6100 directly — the running nooklet serve — rather than bundling the web assets. The window is a native macOS…
- Page-level tags, and how a journal becomes `#Journal`
A block becomes a task by carrying a marker, and ADR-less precedent (see apply-ops.ts's rebuildRefRows) now has every marked block emit a derived tag ref to the Task page. That works because refs…
- Journal pages are stored by ISO date; the title format is a setting
A journal page has three names at once today:
- Templates are core; the journal template is a property of the graph
Logseq's templates are three things: a block with template:: name is a template; /template inserts a copy of its subtree at the caret, with <% today %>-style tokens expanded; and…
- Block/page refactors and graph replace are server ops; a merge rewrites, aliases, and deletes
Four of the most-asked-for things in Logseq's forum (research/13 §4.2, items 3 and 4) are structural edits that touch many blocks at once: turn a block into a page, move a block to a page, merge…
- Linked-reference filters are remembered per device, not as a page property
M7 adds Logseq's linked-references filter (research/13 §4.2 item 5: "Filters for note body", 140 votes, "Sort linked references", 78): on a page's references panel, the set of other pages the…
- The trash never expires; history is the audit log; "restore this version" is a walk of undos
item 10a's asset GC).
- The client plugin host compiles built-in client halves into the web build
the owner yet.
- Pages exist once referenced
Numbering: the brief named this file 023-…; 023-client-plugin-host.md already holds that number, so this is 024.
- A server hosts N graphs, routed by `/g/:graphId/`; a client remembers a list, not one slot
Confirmed") and the "one graph per server" line in docs/spec/00-conventions.md's Graph definition.
- Ops read out of the server's log are applied in `seq` order, not re-sorted by HLC
case with no arbiter; implements what docs/spec/sql-schema.md rule 26 already said for rebuild() ("replaying in seq order").
- The losing text of a same-block conflict becomes a sibling block, minted by the server
research/03-sync.md §6.4's "add a props.conflictcopy with the loser".
- On the desktop, a "local graph" is a new graph on This Mac's bundled server
docs/progress/desktop-local-graph.md. Bug: B-643.
- Pair devices with one-time codes behind an https page; `admin` gates device management
branch, docs/progress/qr-pairing.md).
- Import Logseq DB-version graphs too, from the mirror plus `db.sqlite`
ADR 012 scoped the importer to the classic file graph. The owner's real graph now lives in the Logseq DB version, and importing its Markdown Mirror as if it were a file graph lost data (B-711,…
- Import a Logseq graph from the app: chunked zip upload, server-side import, `admin` only
docs/progress/in-app-import.md). Adds a way in to the importer of ADR 012 (file graphs) and ADR 030 (DB-version graphs); what is imported is theirs, unchanged.