1. Wiki
  2. Driftleaf
  3. Architecture

Architecture

Process Layout

Standard Electron three-process split:

  • src/main — Node-side app logic (window management, vault file access, encryption, search index, IPC handlers). Nothing here talks directly to the renderer — everything goes through IPC and the preload bridge.
  • src/preload — the contextBridge surface exposed to the renderer as window.driftleaf, typed by src/shared/ipc.ts. The renderer never gets raw Node/fs access.
  • src/renderer — React UI built with @aetherAssembly/ui components (Button, Card, Input, Modal, Badge) rather than bespoke chrome. See UI Library below.
  • src/shared — types and IPC channel names shared by main and renderer, so the two sides can't drift out of sync.

Vault Layout

A vault is a folder on disk — one directory per Driftleaf workspace. Folder organization is stored as encrypted metadata rather than mirrored as named note directories on disk.

my-vault/
├── .driftleaf/
│   ├── vault.json        vault metadata: format version, scrypt salt (not the key)
│   ├── canary.enc        known-plaintext blob, used to verify a passphrase on unlock
│   ├── manifest.enc      encrypted note, folder, database, extension, and canvas metadata
│   ├── notes/
│   │   └── <note-id>.enc  encrypted note content under an opaque filename
│   └── <database-id>.db.enc  encrypted database schema, rows, and views

Note titles, folder placement, database titles and IDs, timestamps, extension code and permissions, and canvas layout are encrypted in manifest.enc. Notes use opaque IDs in their file paths, so titles and folder names are not exposed by the filesystem. Database properties, formulas, rows, and saved views are encrypted in .driftleaf/<database-id>.db.enc. Databases are not stored as Markdown files.

The search index is not persisted to disk. See Search below.

Encryption

Notes and database contents are encrypted at rest — this is not a bolt-on. Implemented in src/main/crypto.ts and src/main/vault.ts:

  • One key per vault, AES-256-GCM, in one of two modes (vault.json's kdf field). Encryption itself is never optional — only the passphrase gate varies:
  • "scrypt" — key derived from a user passphrase via scrypt (N=2^17, r=8, p=1, maxmem=256MB) and a stored salt. Nothing about the key is recoverable without the passphrase.
  • Legacy "none" — old vaults can still be opened with an empty passphrase for compatibility, but Driftleaf no longer creates new vaults in this mode. A key stored directly in vault.json (keyHex) means any attacker with vault access can decrypt the contents, so this path is intentionally disabled for new vault creation.
  • Note content, manifest metadata, and each database payload are encrypted with AES-256-GCM, framed on disk as [iv][authTag][ciphertext], regardless of which KDF mode the vault uses.
  • Note files use opaque IDs under .driftleaf/notes/; database schemas, rows, and views use .driftleaf/<database-id>.db.enc.
  • A canary file (canary.enc, a known plaintext string encrypted with the vault key) lets unlock fail fast with "incorrect passphrase" instead of surfacing a raw AES-GCM auth-tag error. Also doubles as a corruption check for "none"-mode vaults, which have no passphrase to get wrong.

Design Decisions

  • Encrypted manifest: titles, IDs, folders, and other vault metadata are protected in manifest.enc. Opaque note filenames avoid exposing titles through the filesystem. The manifest is decrypted after the passphrase check and kept in memory while the vault is unlocked.
  • No recovery path (explicit decision, not an oversight): for "scrypt"-mode vaults, a lost passphrase means encrypted content and metadata are unrecoverable. UnlockScreen gates vault creation with a confirmation modal stating this plainly before the vault is created. See Vault Recovery for the full story.

Vault Resilience

Vault writes are structured so a crash or power loss mid-operation can't corrupt or silently lose a note. Three mechanisms, all in src/main/vault.ts:

  • Atomic file writes (writeFileAtomic): encrypted note and manifest writes go to a sibling temp file first, then land via rename(), which POSIX and NTFS both guarantee is atomic within a directory. A crash mid-write leaves either the old file intact or nothing — never a half-written, unparseable one.

  • Safe operation ordering + self-healing reconciliation: content and manifest writes are atomic, and manifest changes precede deletion of note files. reconcileVault() runs automatically on every unlockVault() call and migrates supported legacy note files to opaque-ID paths. It can recover some orphaned ciphertext, but opaque filenames do not reveal the original title or folder metadata.

  • Manifest entries whose note file isn't on disk → dropped (dangling)
  • Unclaimed opaque-ID files → recovered with placeholder metadata; their original titles and folders cannot be derived from the filename
  • Legacy named note files → migrated to .driftleaf/notes/<note-id>.enc

Because note titles and locations are stored in the encrypted manifest, a crash cannot expose them through the note-file path. The report of what it fixed (VaultRecoveryReport) is returned through vault:unlock and surfaced as a toast in the renderer.

  • Corruption detection on read: AES-256-GCM's auth tag already fails on any bit-flip or truncation of a .enc file, so readNote() catches that decrypt failure and reports "Note is corrupted and cannot be decrypted" instead of letting a raw crypto error surface.

If Your Vault Won't Open at All

If .driftleaf/vault.json, canary.enc, or manifest.enc is missing or unreadable, the vault can't be unlocked. The manifest contains encrypted names and folder metadata, so note and database files cannot be reconstructed with their original organization from disk alone. Restore the entire .driftleaf/ directory from a backup.

Import

importFiles() (src/main/vault.ts) imports a batch of Markdown notes and databases into the currently selected folder:

  • .md files become notes titled after the filename (minus extension), with the file's contents as note content.
  • .csv files become databases. Column names become properties, common types are inferred, and common headers such as tags, status, URL, email, and phone receive type hints.
  • .json files become databases from an array of objects or a top-level data, results, or records array. Notion-style exported property values and common type hints are normalized where supported.
  • .zip archives (parsed with adm-zip, a pure-JS dependency — no native module rebuild step) are walked for .md, .csv, and .json entries at any depth. The archive's internal folder structure is recreated under the target folder. Other entry types are ignored. Zip paths reject traversal and absolute paths to prevent zip-slip. Archives are limited to 20,000 entries and imported entries to 20 MiB each.

Database tables support editable rows and properties, formula properties, relations to other Driftleaf databases, and basic rollups. Notion CSV exports do not include formula definitions, and formulas/relations are not guaranteed to transfer from JSON exports; formulas can be recreated in Driftleaf and relations between imported databases must be set up manually. Formula evaluation is a safe, limited implementation rather than full Notion formula-language compatibility.

One bad file or archive entry doesn't abort the batch — failures are reported as skipped items so a single malformed file doesn't lose the rest of the import. See DATABASES.md for user-facing import, editing, formula, and relation details.

Extensions and Canvas

Extensions run in isolated windows with network access disabled by the app and begin with no permissions. Vault extension code and permission grants are encrypted in the manifest. Extensions can receive scoped, read-only database access when the user grants it. See Extensions for permissions and API details.

Canvas is a spatial workspace for references to notes and database views. Choose items from the sidebar, arrange their cards, and use Open to navigate to the target. Card references and positions are encrypted in the vault; removing a card does not delete its target, while deleting a note or database removes its card. Each vault has one shared canvas layout, with a limit of 500 cards.

Driftleaf can check GitHub for public update and community release metadata. These requests do not include vault contents, note titles, passphrases, or account identifiers. Windows installer builds support user-approved in-app downloads; Linux and unsigned macOS builds link to releases for manual installation. See Software Updates and Community Gallery.

Search

Local full-text search via SQLite FTS5 (src/main/search.ts), indexing note content.

The index is better-sqlite3 opened against :memory: and rebuilt from decrypted content each time a vault is unlocked (buildIndex), then kept in sync incrementally on every write (reindexNote) and delete (removeFromIndex). It is never written to disk, so no encrypted-index format was needed.

Search performance: index building uses parallel decryption and transaction-wrapped inserts, with optional timing instrumentation. Enable it with DEBUG_SEARCH=1 before running npm run dev; main-process timing logs appear in the terminal running the app. See Performance for benchmarking details.

Search queries are tokenized into quoted-prefix terms ("word"* per word) before being handed to FTS5's MATCH, so free-text user input can't inject FTS5 query syntax (column filters, boolean operators).

Packaging

Electron packaging config is split by platform rather than one shared electron-builder.yml, mirroring Before-Its-Gone's build process:

electron-builder/
├── linux.yml         linux.beta.yml    AppImage + deb + rpm
├── macos.yml         macos.beta.yml    dmg + zip
└── windows.yml       windows.beta.yml  nsis installer + portable
build/                                  electron-builder buildResources
├── icon.png / icon.svg                 app icon (1024×1024 source; electron-builder
│                                        derives .icns/.ico from the PNG)
├── agpl.txt                            full AGPL-3.0 text (copy of repo LICENSE),
│                                        shown as the second Windows license page
├── pre-install.txt / post-install.txt  Windows installer welcome/finish page text
└── installer.nsh                       custom NSIS macros wiring the above into the
                                         installer flow (welcome/license/finish pages)
assets/
├── eula.rtf                            Windows installer EULA (first license page)
└── installer-sidebar.png / .svg        164×314 NSIS welcome/finish page sidebar image

Each platform has a :beta config building under a distinct appId / productName (Driftleaf-Beta), so a beta build can be installed side-by-side with a stable one. package.json's package:* / package:*:beta scripts run npm run build (typecheck + Vite bundle, no packaging) followed by electron-builder --config electron-builder/<platform>.yml; publish:* scripts are the same with --publish always, pushing to the Driftleaf GitHub release matching the current version. All of this produces unsigned dev builds; code signing and notarization are planned but not yet implemented.

UI Library

Driftleaf's renderer is built on @aetherAssembly/ui (published to GitHub Packages, see .npmrc), the shared AetherAssembly component library, instead of bespoke UI primitives. Button, Card, Input, Modal, and Badge cover the current screens (unlock flow, sidebar, editor toolbar). It requires React 19 as a peer dependency, which is why Driftleaf is on React 19 rather than 18.

App-specific layout (sidebar/editor grid, split-pane preview) is plain CSS in src/renderer/app.css, layered on top of @aetherAssembly/ui's --ae-* design tokens rather than overriding its components' internals.

Version: 0.6.3 Last updated: 2026-10-12 AetherAssembly/Driftleaf → License: AGPL-3.0-only