Ctrl K
ComunidadGuías…

ARCHITECTURE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Beaver Notes — Architecture Overview

This document describes the high-level architecture for new contributors and AI agents working on the codebase.

## Stack

| Layer | Technology |
|-------|-----------|
| Desktop shell | Tauri 2 (Rust) |
| Frontend | Vue 3 + Pinia + TipTap (rich-text editor) |
| Database | SQLite via rusqlite + r2d2 connection pools |
| Collaboration | Yjs 13 (JS) + yrs 0.27 (Rust) — wire-compatible CRDT protocol |
| Encryption | AES-256-GCM + XChaCha20-Poly1305 (AEAD), Argon2 KDF |
| Build | Yarn 1 (classic), Cargo, Vite |

## Directory Structure

```
src-tauri/src/
  lib.rs              # App entry — plugin registration, invoke handler, run loop
  db.rs               # SQLite pool init, migrations, KV + Yjs helpers
  bootstrap.rs        # App setup, window creation, file-open queue
  secure_blob.rs      # In-memory encrypted blob cache
  commands/
    app.rs            # App lifecycle commands (info, zoom, notifications)
    fs.rs             # Filesystem operations (asset encryption hooks)
    storage.rs        # KV storage commands (storage_get/set/delete, ae:4 row crypto)
    security.rs       # Encryption, key derivation, safe storage, lockout
    yjs.rs            # Yjs CRDT sync commands
    workspace.rs      # Multi-workspace CRUD
    dialogs.rs        # Native dialogs
    ...
  shared/
    mod.rs            # AppState, window helpers, constants
    state.rs          # CryptoSession, SecurityState, CacheState, FileState
    crypto/
      mod.rs          # Encryption module glue
      keys.rs         # Key hierarchy: KDF, KEK/DEK, manifest, rotation, recovery
      assets.rs       # BNA3 asset streaming + BNY1 Yjs blob crypto
      legacy.rs       # CryptoJS (AES-CBC) legacy decryption — migration only
    cache.rs          # LRU decrypted note/asset caches
  secure_blob.rs      # Secure blob store (keyring + encrypted disk fallback)

src/
  index.js            # Frontend entry
  lib/
    tauri/
      commands.js     # IPC bridge — 117-entry command alias map + payload normalization
    tiptap/
      index.js        # TipTap editor setup + extension registration
      exts/           # Custom TipTap extensions (callouts, file-blocks, math, etc.)
  store/              # Pinia stores (note, folder, label, etc.)
  components/         # Vue components
```

## Data Flow

```
Vue Component → Pinia Store → invokeCommand (commands.js) → Tauri IPC → Rust Command → db.rs → SQLite
```

### IPC Boundary (`commands.js`)

The JS→Rust bridge uses Tauri's `invoke` API. `commands.js` maintains:
- A **117-entry alias map** from camelCase JS names to snake_case Rust commands.
- A **`normalizePayload`** switch that remaps params per command (camelCase↔snake_case).
- `withKeyVariants` sends both casing styles for backward compatibility.

**No shared schema or type checking exists.** Changes on either side must be kept in sync manually.

## CRDT Sync (Yjs ↔ yrs)

The real-time collaboration system uses two implementations of the same protocol:

- **JS side:** `yjs` library (v13.6.31) — runs in the browser, manages the editor's in-memory document.
- **Rust side:** `yrs` library (v0.27.2) — runs in the backend, persists updates to SQLite and merges snapshots.

Both encode/decode the same binary format (`updates/v1`), ensuring wire compatibility.

### Sync flow

1. **Editor → Backend (save):** JS calls `yjs_append` with a binary Yjs update blob.
2. **Backend storage:** The raw update is appended to `note_content` (append-only) and folded into a cached merged snapshot in `yjs_snapshots` (O(1) on read).
3. **Backend → Editor (load):** JS calls `yjs_get_snapshot` → Rust decodes all updates via `yrs`, merges into a single state vector, caches the result, and returns the binary snapshot.
4. **Compaction:** `yjs_compact` replaces the append-only history with a single compressed snapshot row.

### Tables involved

| Table | Purpose |
|-------|---------|
| `note_content` | Append-only Yjs binary updates (one row per edit) |
| `yjs_snapshots` | Cached merged snapshot per note (O(1) read) |
| `notes_fts` | Full-text search index (rebuilt from `kv` store) |
| `kv` | Key-value store for note metadata (JSON blobs) |

## Encryption Architecture

> Full, current reference: **[`docs/encryption.md`](docs/encryption.md)** (key hierarchy,
> envelope formats, sequence diagrams, file index, known gaps).

### Layers

| Layer | Algorithm | Scope |
|-------|-----------|-------|
| Note content at rest (Yjs) | AES-256-GCM (`BNY1`) | `note_content` + `yjs_snapshots` blobs |
| Note/KV metadata | XChaCha20-Poly1305 (`ae:4` + per-row AAD) | `data.db` KV rows (`notes.<id>`, `labels`, …) |
| Asset files | AES-256-GCM (`BNA3`, streamed chunks) | Images, attachments in `assets/` (legacy `notes-assets`/`file-assets` are a migration-only source, never written) |
| Sync payload | XChaCha20-Poly1305 (`v4` + AAD) | Cross-device commits/snapshots |
| Secure blobs | AES-256-GCM (OS keyring master key) | Passphrase auto-unlock, Beaver account session |
| Collaboration | AES-256-GCM (WebCrypto, ML-KEM-derived key) | In-session collab updates |
| Key derivation | Argon2id (16 MiB/2/2) | Passphrase → KEK (manifest v3+) |
| Password storage | Bcrypt | Account-password hash verification |

### Key hierarchy

```
User passphrase
  └→ Argon2id → KEK (key-encryption-key, memory only)
       └→ AES-256-GCM wrap → Items key (DEK, random 32 B)
            ├→ XChaCha20-Poly1305: note content (ae:3), KV rows (ae:4), sync (v4)
            └→ AES-256-GCM: Yjs blobs (BNY1), assets (BNA3)
Recovery code (256-bit) ── wraps DEK → recoveryKek (unlock without passphrase)
OS keyring master key   ── AES-GCM → secure blobs
```

One DEK encrypts everything; passphrase only unwraps the stored DEK, so
passphrase changes re-wrap without re-encrypting data. Key rotation archives the
old DEK into `previous_keys` (decryptable via `kid` lookups).

### At-rest boundaries (important)

- `data.db` KV rows are **encrypted** (`ae:4`) when app encryption is enabled and
  the key is loaded; rows written while locked are plaintext and upgraded on read.
- Note content lives in **Yjs CRDT blobs** (`BNY1`), not KV.
- `settings.db`, the FTS5 search index, and the macOS Spotlight index are
  **plaintext by design** (search trade-off).
- The renderer never holds the KEK/DEK — all at-rest crypto runs in Rust.

### Encryption manifest

Stored at `<appData>/app-crypto/manifest.v2.json` (format version 4). Tracks:
- KDF parameters and salts (Argon2id)
- Wrapped key envelopes (nonce + ciphertext)
- `currentKeyId` + rotation history (`previous_keys`)
- Recovery-key envelope (`recoveryKek`)
- Scope (`app` or `sync`)

A copy of the public KDF params + wrapped DEK is published to
`<syncPath>/BeaverNotesSync/keyParams.json` so a second device can derive the
same key from its passphrase.

## Database Schema

Two SQLite databases per workspace:

| DB | Tables | Path |
|----|--------|------|
| `data.db` | `kv`, `note_content`, `yjs_snapshots`, `notes_fts` | `<workspace>/data/data.db` |
| `settings.db` | `kv` | `<workspace>/settings.db` |

**Schema versioning:** `PRAGMA user_version` is used to track and migrate schema. See `db.rs:open_pool()`.

## Multi-Workspace Architecture

Workspaces are isolated directories, each containing `data.db` + `settings.db`. A registry (managed by `workspace.rs`) tracks all known workspaces. Switching workspaces swaps the active database pool.

## Build & CI

- `yarn lint` (oxlint) — gates every deploy
- `yarn audit` — npm dependency audit
- `cargo audit` — Rust dependency audit (RUSTSEC advisories)
- `yarn knip` — dead code detection
- Version consistency check — `package.json` ↔ `tauri.conf.json` ↔ `Cargo.toml`
- **No tests run in CI** (E2E suite exists but requires a display server)