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— thecontextBridgesurface exposed to the renderer aswindow.driftleaf, typed bysrc/shared/ipc.ts. The renderer never gets raw Node/fs access.src/renderer— React UI built with@aetherAssembly/uicomponents (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'skdffield). 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 invault.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.UnlockScreengates 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 viarename(), 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 everyunlockVault()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
.encfile, soreadNote()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:
.mdfiles become notes titled after the filename (minus extension), with the file's contents as note content..csvfiles become databases. Column names become properties, common types are inferred, and common headers such as tags, status, URL, email, and phone receive type hints..jsonfiles become databases from an array of objects or a top-leveldata,results, orrecordsarray. Notion-style exported property values and common type hints are normalized where supported..ziparchives (parsed withadm-zip, a pure-JS dependency — no native module rebuild step) are walked for.md,.csv, and.jsonentries 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.