Baalda

Architecture

The four pillars, the two join keys, the three stores on a client, and the specification set behind each.


Baalda has one genuinely hard problem: the same note is a file on your disk and a CRDT document being edited live by other people, and both have to stay true at once. Most of the architecture exists to serve that.

The four pillars

  • Desktop app. A Tauri 2 shell with a React UI, the CodeMirror 6 editor, a virtualized file tree, and a Rust command surface that owns disk access and the watcher. The UI never touches the disk itself.
  • Database architecture. The on-disk vault layout, the local SQLite index, local CRDT persistence, and the server's Postgres and binary document stores.
  • Sync engine. Yjs, the Hocuspocus server, the sync protocol, and the file-to-CRDT bridge with its loop avoidance.
  • Team collaboration. Accounts and organizations, the folder ACL, how permissions gate which documents a client may open, and presence.

Two join keys

  • `doc_id`. The file, the Yjs document, the local index row and the server row all reference the same id. Nothing is keyed by path, which is why a rename or a move never forks a note.
  • The session. It mints short-lived per-document sync tokens, and the editing server enforces them. Identity and access flow through that one chokepoint.

Three stores on every client

StoreTechnologyHoldsRebuildable
Vault files.md and attachments on diskThe notes themselves: what you, your AI and git touchIt is the truth
Local indexSQLiteSearch, backlinks, tags, the folder tree, the id to path map, sync cursorsYes, from the files
CRDT storeYjs updates in SQLiteLive document state and the offline edit logYes, from the files or the server

What the local index holds

TablePurpose
notesThe id to path map, with title, mtime, content hash and parsed frontmatter
notes_ftsFTS5 full-text search over title and body
tags, note_tagsTags from inline #tags and the tags: property
linksOne row per wikilink, with the resolved target or null when it dangles
foldersThe folder tree, nested by parent
yjs_updates, yjs_snapshotLocal CRDT history and its periodic compaction

On a rename, link targets are updated by id and never by path. That single rule is why moving a note cannot break the links pointing at it.

On the server

  • A binary Yjs store holding opaque document updates and periodic snapshots. Your Markdown never travels the wire in plain text.
  • A relational store for identity, organizations, members, permissions, the registry and attachment metadata.
  • Attachment bytes, in the database or in S3-compatible storage.

The specifications

DocumentCovers
REQUIREMENTSThe 12 core requirements and supporting features, each mapped to a spec and a phase
00-architecture-overviewHow the pillars fit, the bridge, the data-flow diagram, the principles
01-desktop-appThe Tauri shell, the Rust command surface, the editor and the file tree
02-database-architectureVault layout, the index schema, the server's stores
03-sync-engineThe sync protocol, the bridge algorithm and loop avoidance
04-team-collaborationThe organization model, the ACL, how permissions gate sync, presence
05-vault-sync-engineThe always-on vault channel, the stateless relay and pluggable fanout

This page is written from docs/specs/00-architecture-overview.md, docs/specs/02-database-architecture.md and docs/Baalda.md in the repository. Those documents go further, and they are the ones that change first.

Read it on GitHub