A local-first notebook for macOS. One canvas, infinite blocks, full-text search, and SQLite under the hood.
Built with Tauri 2 (Rust + native WebKit), React 19, Tiptap 3, and rusqlite with FTS5.
- One-document canvas — heading-aware ProseMirror editor with slash menu, bubble menu, drag-to-reorder, and an in-line bullet/numbered/task list system.
- Inline hashtags — type
#anything, autocomplete from existing tags, and view aggregated tag pages with reorder / merge / split / group / delete actions. - Tags view with three sort orders (Canvas / Newest / Oldest) and bulk actions:
- Group on canvas — collect tagged blocks into a contiguous run on the main canvas.
- Merge — combine selected blocks into one.
- Split — break each top-level chunk (paragraph / heading / list item) of selected blocks into its own block.
- Export .md — write selected blocks to a markdown file via save dialog.
- ⌘Z undo for every structural action above.
- Search — Slack-style top bar.
- In canvas view: typing shows a read-only list of matching blocks; click jumps to the canvas.
- In tags view: query inline-filters the visible block list (intersected with the active tag, if any).
- Daily SQLite backups — kept under
.notesapp/backups/, browsable + restorable from Settings, with rolling 60-day retention. - Backup preview — open any backup file as read-only and inspect block count, headings, and timestamps before restoring.
- Per-block version history — every content edit records a
block_versionsrow; restorable via the block menu. - Workspace switcher / mover — choose any folder, or move the current
.notesapp/data to a new location. - Markdown export — full-canvas export to
canvas.mdon demand. - Agent-friendly — SQLite is the source of truth. Any agent (or
sqlite3CLI) can write directly toblocks.dband Mochi picks up the changes within ~2s via an mtime poller.
~/Library/Application Support/com.mochi.notes/default/
├── .notesapp/
│ ├── blocks.db ← SQLite source of truth (WAL mode)
│ ├── blocks.db-wal
│ └── backups/
│ └── blocks-YYYY-MM-DD-HHMMSS.db
└── canvas.md.legacy-pre-sqlite ← (only on workspaces migrated from v1)
blocks.db schema (simplified):
blocks {
id TEXT PK, -- ULID
parent_id TEXT, -- heading-tree parent
position INTEGER, -- canvas order
heading TEXT, heading_level INTEGER,
content TEXT, -- block markdown
content_hash TEXT, -- sha256 of content
tags TEXT (JSON), -- derived from inline #tags
manual_tags INTEGER, -- legacy, always 0 in v0.1+
created_at INTEGER, updated_at INTEGER
}
block_versions { id, block_id, content_hash, content, parent_hash, edited_at, source }
blocks_fts (FTS5 over content + tags + heading)
settings { key, value } -- holds schema_version, etc.Schema is currently at version 3. Migration from older versions runs once on app open.
ProseMirror tx → snapshot blocks → ipc.saveBlocks(blocks, deletedIds)
→ Rust save_snapshot
→ UPSERT (skipping no-op rows)
→ re-extract hashtags
→ FTS sync + version snapshot if hash changed
Editing a block in the canvas debounces a per-block save through this same path (300 ms in canvas, 400 ms per row in tags view). Whole-canvas reconcile is gone — there's no markdown round-trip on save, only on explicit export.
src/App.tsx— top-level layout, view-mode + tag-filter + search-query state.src/editor/CanvasEditor.tsx— Tiptap editor with the custommochiBlocknode, drag handles, slash + bubble menus, hashtag autocomplete.src/tags-view/TagsView.tsx— read/write block list, sort/filter/select/bulk-actions.src/topbar/TopBarSearch.tsx— controlled search input.src/sidebar/Sidebar.tsx— Canvas + Tags tabs with the heading tree and tag list.src/settings/SettingsModal.tsx— workspace + backups settings.src/stores/workspace.ts— Zustand store. Ownsblocks,tags,lastMtime, and the tags-view undo stack.
db.rs— schema, queries, save snapshot, backup API.parser.rs— slim helpers (hash,extract_hashtags) plus amigrationsubmodule used only when importing a pre-v2canvas.md.state.rs— workspace open/close/reopen, schema migration, intro seeding.commands.rs— Tauri IPC: bootstrap, switch/move workspace, save_blocks, list/create/restore/preview backup, export canvas, write_text_file, blocks_mtime.config.rs— config file under app-data-dir tracking the active workspace path.
pnpm install
pnpm tauri devDefault workspace lives at ~/Library/Application Support/com.mochi.notes/default/. Switch or move it from Settings → Workspace.
cd src-tauri && cargo test(Currently exercises hashtag extraction + the legacy parser used only for migration.)
Unsigned build (works on your machine; Gatekeeper warning on others):
pnpm tauri buildOutput: src-tauri/target/release/bundle/dmg/Mochi_<version>_<arch>.dmg.
Universal (Apple Silicon + Intel) build:
rustup target add x86_64-apple-darwin aarch64-apple-darwin
pnpm tauri build --target universal-apple-darwinYou need an Apple Developer Program membership ($99/year) and a Developer ID Application certificate installed in Keychain.
-
Copy the env template and fill it in:
cp .env.example .env.local
Edit
.env.localand set:APPLE_SIGNING_IDENTITY— exact certificate name from Keychain (find withsecurity find-identity -p codesigning -v)APPLE_ID— your Apple ID emailAPPLE_PASSWORD— an app-specific password, not your real Apple ID passwordAPPLE_TEAM_ID— your 10-character Team ID from developer.apple.com → Membership
.env.localis gitignored so it never lands in commits. -
Run the release script:
./scripts/release-mac.sh # current arch only ./scripts/release-mac.sh --universal # Intel + Apple Silicon
The script sources
.env.local, runspnpm tauri build, and Tauri picks up the env vars automatically —codesignruns first, thenxcrun notarytool, thenxcrun stapler. Resulting DMG installs cleanly on any Mac.
MIT © 2026 Samuel Liu.