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
| Store | Technology | Holds | Rebuildable |
|---|---|---|---|
| Vault files | .md and attachments on disk | The notes themselves: what you, your AI and git touch | It is the truth |
| Local index | SQLite | Search, backlinks, tags, the folder tree, the id to path map, sync cursors | Yes, from the files |
| CRDT store | Yjs updates in SQLite | Live document state and the offline edit log | Yes, from the files or the server |
What the local index holds
| Table | Purpose |
|---|---|
notes | The id to path map, with title, mtime, content hash and parsed frontmatter |
notes_fts | FTS5 full-text search over title and body |
tags, note_tags | Tags from inline #tags and the tags: property |
links | One row per wikilink, with the resolved target or null when it dangles |
folders | The folder tree, nested by parent |
yjs_updates, yjs_snapshot | Local 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
| Document | Covers |
|---|---|
REQUIREMENTS | The 12 core requirements and supporting features, each mapped to a spec and a phase |
00-architecture-overview | How the pillars fit, the bridge, the data-flow diagram, the principles |
01-desktop-app | The Tauri shell, the Rust command surface, the editor and the file tree |
02-database-architecture | Vault layout, the index schema, the server's stores |
03-sync-engine | The sync protocol, the bridge algorithm and loop avoidance |
04-team-collaboration | The organization model, the ACL, how permissions gate sync, presence |
05-vault-sync-engine | The 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 GitHubWas this page helpful?
