# nooklet > nooklet is a local-first outliner in the spirit of Logseq. Notes are nested bullets with [[links]], #tags and tasks, stored in SQLite with a plain markdown mirror on your own disk. Every device keeps a full replica and syncs through a server you run, using an op log with hybrid logical clocks and per-field last-writer-wins. Agents can read and edit the graph over HTTP or MCP with scoped tokens. Every page below is plain markdown. The HTML version is the same URL without `.md`. Source code and issues: https://github.com/hnykda/nooklet ## Guide - [What nooklet is](https://nooklet.danielalder.cz/docs/what-is-nooklet.md): A local-first outliner in the spirit of Logseq, built so AI agents can read and write your notes properly. - [Features](https://nooklet.danielalder.cz/docs/features.md): A tour of what nooklet does today, with partial and missing pieces marked. - [How it works](https://nooklet.danielalder.cz/docs/how-it-works.md): SQLite on every device, an append-only op log with hybrid logical clocks, last-writer-wins fields, fractional ordering, and a markdown mirror. - [Importing from Logseq](https://nooklet.danielalder.cz/docs/importing-from-logseq.md): Bring a Logseq graph into nooklet, from either the classic file-based Logseq or the newer database (DB) version, and what carries over from each. - [Sync, offline and conflicts](https://nooklet.danielalder.cz/docs/sync-and-offline.md): What happens when a device goes offline, what the sync indicator means, how conflicts resolve, and how local-only and multiple graphs work. - [Getting started](https://nooklet.danielalder.cz/docs/getting-started.md): Download or build nooklet, run a server, open the web, desktop, iOS and experimental Android apps, and pair devices with tokens or a pairing link. - [Self-hosting](https://nooklet.danielalder.cz/docs/self-hosting.md): Run the nooklet server behind Tailscale (recommended), in Docker or Kubernetes, or behind a public reverse proxy; back it up, upgrade it, and add semantic search with Ollama. - [Build and install nooklet on your iPhone](https://nooklet.danielalder.cz/docs/ios-from-source.md): Build nooklet with Xcode and install it on your own iPhone with a free Apple ID, connect it to your server, and debug it with Safari's Web Inspector. - [Security model](https://nooklet.danielalder.cz/docs/security.md): What nooklet protects against, how tokens and scopes work, what the server trusts, and the recommended deployment. - [For AI agents](https://nooklet.danielalder.cz/docs/agents.md): Connect Claude Code, Cursor or any MCP client to nooklet, or call the HTTP API directly. Tools, tokens, the outline format and a worked example. - [Building from source and the toolchain](https://nooklet.danielalder.cz/docs/building.md): The toolchain, the monorepo map, every build command and what it produces, the test layers, CI, and fixes for the build problems people have hit. - [FAQ and troubleshooting](https://nooklet.danielalder.cz/docs/faq.md): Fixes for the problems people hit in real testing, from secure-context errors and Host 403s to the iOS local-network prompt. ## Optional Design decisions (architecture decision records): - [ADR 001: Stack and tooling](https://nooklet.danielalder.cz/decisions/001-stack-and-tooling.md): 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,… - [ADR 002: SQLite is the source of truth; markdown files are a lossless mirror](https://nooklet.danielalder.cz/decisions/002-storage-sqlite-truth-markdown-mirror.md): 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… - [ADR 003: Sync = op log + hybrid logical clocks + per-field last-writer-wins, server validates the tree](https://nooklet.danielalder.cz/decisions/003-sync-oplog-hlc-lww.md): 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… - [ADR 004: Short time-ordered ids and fractional-index ordering](https://nooklet.danielalder.cz/decisions/004-ids-and-ordering.md): 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… - [ADR 005: Client packaging: PWA first, Capacitor for stores, Tauri for desktop](https://nooklet.danielalder.cz/decisions/005-mobile-and-packaging.md): 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,… - [ADR 006: Editor: rendered blocks plus one re-parented CodeMirror 6 surface](https://nooklet.danielalder.cz/decisions/006-editor.md): 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… - [ADR 007: Plugins: one package, optional server and client halves, trusted ESM in v1](https://nooklet.danielalder.cz/decisions/007-plugins.md): 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… - [ADR 008: One operation registry for HTTP, MCP, and the typed client](https://nooklet.danielalder.cz/decisions/008-api-and-mcp.md): 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 +… - [ADR 009: Every operation is a command; keybindings are user data](https://nooklet.danielalder.cz/decisions/009-commands-and-keybindings.md): 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… - [ADR 010: Embeddings via Ollama, vectors in sqlite-vec, hybrid search by rank fusion](https://nooklet.danielalder.cz/decisions/010-embeddings-and-search.md): 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… - [ADR 011: Scheduling, repeats, and query blocks use one property/fence syntax, not org-mode](https://nooklet.danielalder.cz/decisions/011-scheduling-and-queries-syntax.md): 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).… - [ADR 012: The importer targets the Logseq file graph only, not the DB version's markdown export](https://nooklet.danielalder.cz/decisions/012-import-scope-file-graph-only.md): too, from their mirror plus db.sqlite. - [ADR 013: AI parity is a first-class requirement — undo and assets in MVP, live UI control designed for M2](https://nooklet.danielalder.cz/decisions/013-ai-parity-undo-assets-live-ui.md): M2, not yet an implementation decision. - [ADR 014: Stay on Node for the server; Bun is a viable later swap, not a now decision](https://nooklet.danielalder.cz/decisions/014-runtime-node-not-bun.md): 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… - [ADR 015: Live UI control — a dedicated `/ui/live` socket, the existing command registry, consent by default-asymmetry](https://nooklet.danielalder.cz/decisions/015-live-ui-control-channel.md): Follows from ADR 013's forward-looking item. Full survey and rationale: docs/research/09-live-ui-control.md. - [ADR 016: Desktop app — Tauri, pointed at the local server](https://nooklet.danielalder.cz/decisions/016-desktop-shell-tauri.md): 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… - [ADR 017: Page-level tags, and how a journal becomes `#Journal`](https://nooklet.danielalder.cz/decisions/017-page-level-tags.md): 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… - [ADR 018: Journal pages are stored by ISO date; the title format is a setting](https://nooklet.danielalder.cz/decisions/018-iso-journal-names.md): A journal page has three names at once today: - [ADR 019: Templates are core; the journal template is a property of the graph](https://nooklet.danielalder.cz/decisions/019-templates.md): 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… - [ADR 020: Block/page refactors and graph replace are server ops; a merge rewrites, aliases, and deletes](https://nooklet.danielalder.cz/decisions/020-refactor-ops-merge-semantics.md): 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… - [ADR 021: Linked-reference filters are remembered per device, not as a page property](https://nooklet.danielalder.cz/decisions/021-reference-filters-per-device.md): 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… - [ADR 022: The trash never expires; history is the audit log; "restore this version" is a walk of undos](https://nooklet.danielalder.cz/decisions/022-trash-history-retention.md): item 10a's asset GC). - [ADR 023: The client plugin host compiles built-in client halves into the web build](https://nooklet.danielalder.cz/decisions/023-client-plugin-host.md): the owner yet. - [ADR 024: Pages exist once referenced](https://nooklet.danielalder.cz/decisions/024-pages-exist-once-referenced.md): Numbering: the brief named this file 023-…; 023-client-plugin-host.md already holds that number, so this is 024. - [ADR 025: A server hosts N graphs, routed by `/g/:graphId/`; a client remembers a list, not one slot](https://nooklet.danielalder.cz/decisions/025-multi-graph-hosting.md): Confirmed") and the "one graph per server" line in docs/spec/00-conventions.md's Graph definition. - [ADR 026: Ops read out of the server's log are applied in `seq` order, not re-sorted by HLC](https://nooklet.danielalder.cz/decisions/026-server-log-applied-in-seq-order.md): case with no arbiter; implements what docs/spec/sql-schema.md rule 26 already said for rebuild() ("replaying in seq order"). - [ADR 027: The losing text of a same-block conflict becomes a sibling block, minted by the server](https://nooklet.danielalder.cz/decisions/027-conflict-copy-as-sibling-block.md): research/03-sync.md §6.4's "add a props.conflictcopy with the loser". - [ADR 028: On the desktop, a "local graph" is a new graph on This Mac's bundled server](https://nooklet.danielalder.cz/decisions/028-desktop-local-graphs-on-the-bundled-server.md): docs/progress/desktop-local-graph.md. Bug: B-643. - [ADR 029: Pair devices with one-time codes behind an https page; `admin` gates device management](https://nooklet.danielalder.cz/decisions/029-qr-pairing-one-time-codes.md): branch, docs/progress/qr-pairing.md). - [ADR 030: Import Logseq DB-version graphs too, from the mirror plus `db.sqlite`](https://nooklet.danielalder.cz/decisions/030-import-logseq-db-version-graphs.md): 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,… - [ADR 031: Import a Logseq graph from the app: chunked zip upload, server-side import, `admin` only](https://nooklet.danielalder.cz/decisions/031-in-app-logseq-import.md): 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. All of the above in one file: https://nooklet.danielalder.cz/llms-full.txt