Technical overview

Plain Markdown on disk. Everything else is rebuildable.

Commonplace is an Electron 42 + React 19 desktop app whose renderer also runs as a static browser build. The vault is a folder of .md files; a SQLite index inside .commonplace/ powers search, backlinks, the graph, and semantic memory.

Stack at a glance

One repository powers the desktop app, the static browser build, and the marketing site.

Layer Choice
Build / devVite, TypeScript, esbuild for the Electron entry
UIReact 19, shadcn-derived primitives, lucide-react icons
Desktop shellElectron 42 with a narrow contextBridge preload
Storage (truth)Plain Markdown files in a user-picked folder
Storage (derived).commonplace/index.sqlite via better-sqlite3
SearchSQLite FTS5 on desktop · MiniSearch in browser fallback
File watcherchokidar with debounced batched events
Semantic memoryChunker + provider interface; deterministic placeholder today
TestingVitest unit + component tests
Site / app deliveryCloudflare Pages deploy on push to main

Runtime modes

Same renderer, different storage adapters. Markdown is always the source of truth.

Desktop · primary

Electron + local folder

All filesystem and SQLite work lives in the main process. The renderer talks to it over a typed window.commonplace.* IPC bridge — no raw fs, no ipcRenderer, no shell access in renderer code.

  • Native folder picker · multi-vault switcher
  • chokidar watcher with stale-note banner + write conflict guard
  • SQLite FTS5 search · backlinks · graph · semantic memory
Browser · Chromium-family

File System Access

In Chrome / Edge desktop, Commonplace uses the File System Access API to read and write real Markdown files in a folder you pick. No backend involved.

  • Real .md file IO via the browser folder handle
  • In-memory MiniSearch index for search
  • Backlinks/graph derived in-renderer from wiki links
Browser · fallback

Browser-local demo

For Firefox, Safari, or any browser without folder access. Notes persist in localStorage. Import / export keeps the data portable.

  • No file system access required
  • Attachments intentionally unsupported in this mode
  • Easy upgrade path: export, then re-open as local-folder vault

Storage

Markdown is the contract. SQLite is a cache. Both are recoverable from each other.

Source of truth

A vault is a folder. Commonplace scaffolds a small structure on first use but won't claim your data: notes are plain .md files you can open in any editor and back up like any other folder.

my-vault/
  Notes/                       your notes
  Daily/                       daily-notes folder
  Attachments/                 image/audio/file imports
  Templates/                   note templates
  .commonplace/
    index.sqlite               derived metadata cache
    settings.json              per-vault settings
    workspace.json             tab/state persistence (Electron)

SQLite index (Electron)

better-sqlite3 backs an index of files, frontmatter, links, note_headings, and an FTS5 virtual table notes_fts. Re-indexing is incremental (mtime + content-hash gated) and a full rebuild is always available — every row is recoverable from the Markdown files.

External-edit safety

chokidar watches the vault. The renderer receives debounced batched events; if the file backing the active editor changes externally, a non-modal banner offers Reload or Keep. On save, the adapter passes expectedMtimeMs; the main process returns a conflict marker if the file changed underneath, surfaced as a WriteConflictError in the renderer.

IPC bridge

The renderer is treated as untrusted relative to the main process. Every privileged operation goes through a typed channel.

The Electron preload (electron/preload.ts) exposes window.commonplace.* via contextBridge — no raw ipcRenderer, no Node access in the renderer. The renderer type definition lives in src/types/electron.d.ts.

window.commonplace.platform.runtime  // "electron" | "browser"
window.commonplace.vault             // list, read, write, rename, delete, scaffold
window.commonplace.vaultWatch        // subscribe to file watcher events
window.commonplace.index             // rebuild, getStats, searchNotes, getBacklinks
window.commonplace.semantic          // setEnabled, rebuild, getRelated, getStats
window.commonplace.settings          // load/save per-vault settings

All renderer-supplied paths go through a path-safety guard (electron/services/pathSafety.ts) before they're touched by node:fs/promises. Attachment binary data crosses the bridge as ArrayBuffer.

Search & semantic memory

Lexical search via SQLite FTS5; a semantic scaffold ready for a real local embedding model.

Full-text search

Desktop search runs against the notes_fts virtual table (path, title, body, headings, frontmatter text) through a punctuation-safe tokenizer. The renderer's SearchProvider abstraction switches between browserMiniSearchProvider and desktopSqliteSearchProvider transparently based on runtime.

Related Notes panel

The semantic stack (chunker, EmbeddingProvider interface, semantic_chunks / semantic_embeddings tables) ships with a deterministic placeholder embedding today — useful as scaffolding, not a real model. Stale-request protection (AbortSignal) and a debounced refresh keep the panel responsive across rapid note switches.

Real local embeddings (in progress)

A local-transformers provider exists behind the same interface, with explicit embeddingModelPath settings (no automatic downloads), rebuildNeeded detection so stale embeddings can't get compared across providers, and a clear not-installed state. The runtime model package is intentionally not yet a dependency — the current recommendation is Ollama-over-localhost first.

Security model

No backend, no auth, no telemetry. The threat model is your filesystem.

  • No backend server required for core product behaviour.
  • No user accounts, no auth layer, no app-owned cloud sync.
  • User data stays in the selected folder or browser storage.
  • Renderer is treated as untrusted: contextIsolation, no nodeIntegration, no raw ipcRenderer exposed.
  • Path-safety guard rejects traversal attempts on every IPC entry point.
  • Markdown preview blocks javascript: URLs, unsafe schemes, and script injection vectors.
  • Attachment binaries pass through the main process; the renderer only handles Blob URLs it constructed itself.