Why I built Enclave: notes that can't phone home
Every notes app wants an account and a server. I wanted neither. Here is the threat model, the cryptography, and the hard parts behind a local-first, zero-knowledge knowledge base.
I have a bad habit: I read a privacy policy before I sign up for anything. It is a great way to stop signing up for things.
Every notes app I evaluated wanted the same things. An account. A server I don't control. A sync model where "your data" is a polite way of saying "our database". Some of them are genuinely careful — end-to-end encrypted, audited, transparent. But the moment there is a server, there is a breach surface, a subpoena surface, a shut-down surface, and a pricing change surface.
I wanted a knowledge base where the answer to "where are my notes?" was boring: on my devices. So I set three constraints and refused to negotiate with myself.
- No servers. Not even mine.
- No accounts. No email, no OAuth, no recovery flow that depends on anyone else.
- No telemetry. Not even anonymous crash reports.
That is Enclave: an offline-first, zero-knowledge knowledge base for Windows, Linux, macOS, and Android. It shipped, it syncs, and it has never made a network request I didn't configure.
Start with the threat model, not the cryptography
Cryptography without a threat model is decoration. So before choosing a single primitive, I wrote down what Enclave is defending against:
| Threat | In scope? | How it is addressed |
|---|---|---|
| A server breach leaking notes | Yes, by construction | There is no server to breach |
| A passive observer on your Wi-Fi reading notes mid-sync | Yes | Every sync frame is sealed with XChaCha20-Poly1305 |
| A malicious device on the same network joining the sync | Yes | Mutual challenge-response authentication before any data moves |
| A stolen laptop or phone (locked) | Yes | The vault file is encrypted at rest with SQLCipher |
| A compromised operating system while Enclave is unlocked | No | Out of scope — no app wins this fight |
| Someone who knows your 12 words | Mitigated | The phrase is the key; there is no second factor by design |
That last row is the honest trade-off at the center of zero-knowledge software. If nobody else can recover your data, nobody else can recover your data. Enclave makes you write down twelve words, confirms them, and then gets out of the way.
The vault: one encrypted file
The data model is a document + block structure with fractional indexing, stored in SQLite. The entire database file is encrypted with SQLCipher (AES-256-CBC with HMAC-SHA512), and full-text search plus vector search run inside that encrypted file.
The key derivation chain is:
12-word BIP39 mnemonic
│
▼
Argon2id(password=mnemonic, salt="enclave-vault-master-key-v1")
64 MiB · 3 iterations · 4 parallelism
│
▼
256-bit master key ──► SQLCipher PRAGMA key
│
└── HKDF-SHA256 ──► sync key ──► peer authentication (HMAC-SHA256)
+ transport (XChaCha20-Poly1305)
A few deliberate choices:
- Argon2id, not PBKDF2. Memory-hard derivation makes offline guessing expensive. 64 MiB per attempt matters when an attacker has your disk image.
- BIP39 words, not a password. Twelve words from a curated list are easier to write down and transcribe correctly than a random base64 string, and they carry 128 bits of entropy. People back up words. They do not back up key files.
- HKDF for the sync key. The master key never travels. A separate, domain-separated key handles the network, so compromising a sync session doesn't hand over the vault.
- Keys live in memory only. The mnemonic and derived keys exist for the session and are never written to disk.
Sync that assumes the network is hostile
Sync is peer-to-peer over the local network. No relay, no rendezvous server, no cloud fallback.
Devices discover each other with mDNS (_enclave._tcp.local) and talk over WebSocket. The handshake is mutual: each peer must prove knowledge of the vault-derived sync key with a challenge-response HMAC-SHA256 proof before anything else is exchanged — including the initial hello. Once authenticated, every frame is sealed with XChaCha20-Poly1305 under a per-session key.
That means a stranger on the same café Wi-Fi sees challenges and ciphertext. Wrong-key peers are rejected before they learn anything. And because sync is LAN-only, "cloud outage" is not a category of problem Enclave has.
The sync algorithm itself is intentionally modest: full snapshots with document-level last-write-wins resolved in the Rust core. It is not a CRDT paper. It is correct for the actual usage pattern — a handful of devices owned by one person, editing pages, not collaboratively typing into the same paragraph at the same millisecond. Choosing boring correctness over impressive complexity is a feature.
The hard parts nobody warns you about
Three things took far longer than the cryptography.
First, the seed phrase UX. Twelve words is a lot to ask. The flow has to be strict — force a confirmation re-entry, explain that there is no reset — without feeling like a threat. I wrote and rewrote that screen more than any other.
Second, search inside encryption. SQLCipher gives you an encrypted file, but FTS5 and sqlite-vec need to work on plaintext within the decrypted connection. Getting the vec0 index to page correctly and stay consistent with the document/block model took real care. The payoff: nothing sensitive ever lands in a temp file.
Third, Android. Tauri v2 got the app onto Android from the same Svelte frontend and Rust core, but mobile lifecycle, background sockets, and file access behave very differently from desktop. The signed .apk/.aab beta is built in CI from the same codebase, which keeps the platform from becoming a fork.
The offline RAG assistant
The feature I expected to be impossible: ask questions across your vault, offline.
Enclave stores embeddings in an in-database sqlite-vec ANN index inside the encrypted vault file, re-ranks with exact cosine similarity in Rust, and generates embeddings in-process with ONNX (all-MiniLM-L6-v2). Retrieval never leaves the device. The generation step is optional and pluggable: point it at Ollama, llama.cpp, LM Studio, vLLM, or any OpenAI-compatible endpoint — including a frontier API if you consciously choose to. The default path is fully local.
If you have never used a local model: it is slower and less fluent than the frontier. It is also yours, it works on a plane, and no prompt you type becomes someone's training data. For a private knowledge base, that trade is not close.
What I would tell someone building this
- Write the threat model first. It decides your architecture, your UI copy, and what you refuse to build.
- Encrypt before storage, not after. "We encrypt at rest" means nothing if plaintext hits a journal first.
- Make the secure path the easy path. The default must be the private one; the network feature must be opt-in and local.
- Design for no recovery. Users will lose phrases. Say so clearly, repeatedly, before it happens.
- Zero dependencies is a security feature too. Every dependency you don't ship is a supply-chain incident you can't have.
Enclave is MIT-licensed and developed in the open. If any of this is useful to you, star the repo, file an issue, or fork it and make it yours.