How Filarr's optional cloud sync stays zero-knowledge (a look inside the code)
How Filarr's optional cloud sync stays zero-knowledge: files upload already-encrypted, the server stores only opaque blobs, and your key never reaches it in cleartext.
Mathis Belouar-Pruvot
· founder of Filarr · · 15 min read
You bought a second laptop, or you want your work notes on the machine at home and the one at the office. The moment your files leave one device, you face the same quiet question every cloud tool asks you to ignore: who can read them on the way, and who can read them once they land?
Filarr's answer is that its servers only store encrypted data they cannot read. Your files sync between your devices, but the server that moves them never holds your key in cleartext, never sees a filename, and never touches a byte of readable content. This article walks through exactly how that works, straight from the sync code, because "zero-knowledge" is a claim that only means something if you can see the mechanism behind it.
A note on scope before we start: cloud sync in Filarr is optional. The product is local-first, your files live encrypted on your disk whether or not you ever turn sync on. Everything below describes what happens when you choose to enable it.
Quick Answer
Filarr syncs your files by uploading them exactly as they already sit on your disk: encrypted with AES-256-GCM under your account key (the FEK), with a fresh random value for each file. The FEK never reaches the server in readable form. The Cloudflare Worker that handles sync only ever stores opaque encrypted blobs under opaque file IDs (a SHA-256 hash of the local path), plus a small manifest that is itself AES-256-GCM encrypted with your FEK. The server can see how many blobs you have and how big they are, but not their names, their folder structure, or their content. A second device gets your key either by signing in, which downloads the copy of the FEK wrapped under your password, or device-to-device with an ECDH key exchange, where the FEK travels wrapped and the server only relays public keys.
The problem it actually solves
Most sync is "trust the provider." Notion stores your pages in cleartext on its servers. Dropbox holds the keys to the files it encrypts at rest, which means Dropbox (or anyone who compels Dropbox) can read them. Even tools that encrypt in transit usually decrypt on arrival so they can index, preview, or search your content server-side.
That model is convenient and it is also the whole problem. Convenience server-side means readability server-side. If the provider can generate a thumbnail of your document, the provider can read your document.
Filarr's constraint was the opposite: the sync backend had to be genuinely unable to read anything it stores, while still doing the useful work of moving bytes between your machines and resolving conflicts. "Zero-knowledge" here is not a marketing adjective, it is a design requirement that shaped where encryption happens and what the server is allowed to know.
How it works, step by step
1. Encryption already happened, before sync exists
This is the key idea that makes the rest simple. In Filarr, files are encrypted on disk with AES-256-GCM, under your account key with a fresh random value for each file, the moment they enter your vault. Sync did not add encryption. Sync inherits it.
When the sync service reads a file to upload it, it reads the already-encrypted bytes off disk and sends them as-is. There is no decrypt-then-reencrypt step, no plaintext staging area. The comment at the top of the sync service says it plainly: "Files on disk are already encrypted with FEK, uploaded as-is." The server receives ciphertext because ciphertext is all that ever existed outside the app's memory.
2. Filenames become opaque IDs
A blob of ciphertext is useless to an attacker, but a filename like "resignation-letter-final.pdf" is not. So Filarr never sends filenames.
When a file changes, the sync service derives an identifier by hashing its local path: crypto.createHash('sha256').update(localPath).digest('hex').slice(0, 32). The comment in the code is explicit about the intent: "Derive opaque fileId from local path, never expose real file names." Folders are referenced by their UUIDs, which carry no meaning either. On the server, your files live at a path like users/{userId}/profiles/{profileId}/files/{fileId}/chunk_0.enc, where every segment is either a random ID or an opaque hash.
3. Files are chunked
Before upload, files are split into 4 MB chunks (CHUNK_SIZE = 4 * 1024 * 1024). Each chunk is stored as a separate .enc object in Cloudflare R2. Chunking keeps individual uploads small enough to retry cheaply on a flaky connection, and it means a large file that changed does not force a single monolithic transfer. Uploads run in small parallel batches (three at a time) with a persistent retry queue behind them, so a dropped connection resumes rather than restarts.
4. The manifest: how devices agree on reality
Here is the part that is genuinely interesting, because it is where a zero-knowledge system usually leaks. To sync, two devices need to agree on which files exist, which changed, and which won a conflict. That coordination normally requires the server to understand your file structure. Filarr's server does not.
There are two manifests, and the distinction matters:
The local manifest (sync-manifest.json) lives on your disk and is not encrypted, but it is deliberately empty of anything sensitive. It holds SHA-256 checksums, byte sizes, sync statuses, and opaque file IDs. No filenames, no content. The localPath field that maps an ID back to a real path exists only in this local copy and is stripped before anything goes to the cloud.
The cloud manifest (manifest.enc on R2) is the shared source of truth between devices, and it is encrypted with your FEK using AES-256-GCM (encryptWithFEK). Its on-disk format is a short marker ("fek:") followed by a 12-byte IV and the ciphertext with its GCM authentication tag. The server stores this as one more opaque blob. When a second device wants to know the state of the world, it downloads this blob and decrypts it locally, because it has the FEK. The server never can.
All the merge logic (which file is newer, which changed on both sides and is therefore a conflict) runs on your device, on the decrypted manifest, in mergeWithRemote. The server's only job around the manifest is version control.
5. Optimistic locking, so two devices do not clobber each other
Every profile has a manifest_version counter stored in the Worker's D1 database. When a device wants to upload a new manifest, it tells the server which version it thinks is current. If the server's version does not match, it returns a 409 conflict and the upload is refused (if (profileSync.manifest_version !== body.version) return ... 409). The device then re-fetches, re-merges, and tries again on the next cycle.
This is the elegant trick: the server can enforce "only one writer wins per version" using nothing but an integer. It never needs to understand the manifest's contents to keep two devices from corrupting each other's state. Coordination without comprehension.
6. Uploads and downloads go through a token proxy
Workers cannot mint classic S3-style presigned URLs directly, so Filarr uses a token pattern. The device makes an authenticated request for an upload slot, the Worker checks your storage quota, stores a short-lived intent record in KV (15-minute TTL) keyed by a random token, and hands back a token URL. The device then PUTs the encrypted chunk to that URL, and the Worker streams it into R2 and increments your usage counter. The token is the authorization for that one operation, and it is deleted on use. The bytes that pass through are, again, already-encrypted ciphertext.
7. Getting your key onto a second device without ever sending it in cleartext
Everything above assumes both devices have the FEK. The simplest way to get it there is to sign in on the new device: the server sends the copy of the FEK wrapped under your password, and the device unwraps it locally. The other way skips the password: you pair the new device with one that is already unlocked. So how does pairing get the key across without the server ever seeing it?
Pairing uses an ephemeral ECDH key exchange over the P-256 curve. The flow, from pairingService.ts:
Device A (the one that already has your vault) generates an ephemeral ECDH keypair and a random 6-digit code. Device B generates its own ECDH keypair. Each device publishes its public key to the server, fetches the other's, and independently derives the same 256-bit shared secret. The key that wraps the FEK is derived from that shared secret and from a 32-byte secret that device A puts in the QR code and never sends to the server; in manual mode, without a QR code, both screens show six check digits derived from the exchange, and you confirm they match before anything is wrapped. Device A then wraps the FEK with that key using AES-GCM and uploads the wrapped key. Device B downloads it, unwraps it, and stores it locally, re-wrapped under its own password.
The FEK crosses the wire exactly once, and only in a form that requires the shared secret to open. The comment states the guarantee: "The FEK never leaves a device in cleartext." The server relays public keys and a wrapped blob. It never has the shared secret, so it never has the FEK.
The pairing session is hardened: the 6-digit code expires after 5 minutes, and the server rate-limits public-key retrieval to 3 attempts before invalidating the session entirely, which blunts brute-forcing the code.
8. Where the FEK actually rests
On each device, the FEK is stored wrapped. It is encrypted by a KEK (key-encryption key) derived from your password with PBKDF2-SHA512 at 600,000 iterations (well above the 210,000 OWASP recommends for PBKDF2-SHA512), and separately cached in the OS keychain via Electron's safeStorage. Your password derives the KEK, the KEK unwraps the FEK, and the FEK decrypts your files. The server keeps a copy of the wrapped FEK, so that a new device can sign in, but never the FEK in cleartext.
Design choices and their trade-offs
Encrypt-once at the vault boundary, not at the sync boundary. By making files ciphertext on disk from the start, sync becomes a dumb pipe and the zero-knowledge property falls out for free. The trade-off is that the server literally cannot help you with anything content-aware: no server-side search, no thumbnails, no web preview. That is a real feature loss, and it is the honest cost of the guarantee.
Client-side merge instead of server-side. Running the merge on-device keeps the server ignorant, but it means conflict resolution depends on device clocks and on manifests being fetched in the right order. The merge algorithm leans on updatedAt and syncedAt timestamps, and when both sides changed since the last sync it does not silently pick a winner: it forks, writing a _conflict_{timestamp} copy so nothing is lost. Safer, but it can leave you with duplicates to reconcile by hand.
A token proxy instead of direct-to-R2 presigning. Routing bytes through the Worker adds a hop and some latency versus a true presigned URL straight to storage. In exchange, quota enforcement and access control stay centralized and simple, and since the payload is already encrypted, the extra hop sees nothing.
Chunking at 4 MB. Small chunks make retries cheap and parallelism easy, at the cost of more objects and more round-trips for large files. For a workspace of notes and documents, that is the right bias.
Threat model: what this protects, and what it does not
What an attacker who copies the server's data (or compels the provider to transmit it) gets: a pile of opaque .enc blobs, an encrypted manifest they cannot open, opaque file IDs, a version counter and the wrapped copy of your FEK. Without your password or your recovery phrase, they cannot read your files, your notes, your filenames, or your folder structure. An attacker who controls the server while you use it cannot take your password from an up-to-date app's sign-in: since October 16, 2026, signing in sends a key derived from your password, not the password, and that key cannot unwrap the FEK. During a transition of about two months, though, an account that hasn't signed in again yet, or an older app version, still sends the password itself (over HTTPS), and a modified server could capture it and unwrap that copy.
What that same attacker can still observe (be honest about this): metadata. Specifically the number of files you have, the size of each blob, the number of chunks, your total storage usage, and the timing of your syncs. File sizes and change frequency can leak patterns. This is the classic limitation of any encrypted storage that stores real object sizes, and Filarr does not pad or pool to hide it. If your threat model includes an adversary doing traffic and size analysis, know that this layer is not designed to defeat that.
The pairing window deserves a specific caveat. During pairing, the server relays both public keys, so a malicious server could attempt a man-in-the-middle on the ECDH exchange by substituting its own public keys. In the first version of the protocol, that attack worked: the 6-digit code, which salted the wrap key, is also known to the server. The current version closes it: the wrap key also depends on the secret carried by the QR code, which no server route ever receives, device A commits to its public key before seeing B's, and in manual mode the six check digits shown on both screens reveal a substituted key. It is worth naming rather than pretending away.
Known limits
- Metadata is not hidden. Blob sizes, file counts, and sync timing are visible to the server. Content and names are not.
- Zero-knowledge cuts both ways. Because the server only holds your key wrapped, there is no "forgot password" reset that recovers your data. If you lose your password and your 24-word recovery phrase, the ciphertext is unrecoverable. That is the price of the server being unable to read you.
- No server-side features on your content. Search, previews, and web access to file contents cannot exist server-side by design.
- The move away from password sign-in isn't finished. Since October 16, 2026, an up-to-date app signs in with a derived key. For about two months, an account that hasn't signed in again yet, or an older app version, still sends the password (checked against a bcrypt hash, not kept), which a compromised server could capture, as described above.
- Legacy manifests. Older manifests encrypted with a device-local key are auto-detected and migrated, which is handled transparently but is a reminder that the format has evolved.
A licensing note for the technically curious: the Filarr desktop client is published under BSL 1.1 ("source available"), so the way it encrypts your files on disk is inspectable. The code described in this article is not: sync.ts, syncService.ts and pairingService.ts belong to the cloud modules, which are not published. The quotes above come from that private code.
Technical FAQ
Q: Does the encrypted content ever get decrypted on the server, even briefly? No. Files are already AES-256-GCM ciphertext on disk before sync runs, and they are uploaded byte-for-byte in that form. The Worker streams blobs into R2 without ever holding a key. There is no decrypt step anywhere server-side.
Q: How does a second device coordinate sync if the server cannot read the manifest? The cloud manifest is itself encrypted with your FEK. Each device downloads it, decrypts it locally, and runs the full merge on-device. The server only enforces an integer version counter (optimistic locking) to prevent two devices from overwriting each other, which it can do without understanding the contents.
Q: How does the encryption key reach my new device without touching the server in cleartext? Either by signing in, which downloads the copy of the FEK wrapped under your password, or through an ephemeral ECDH exchange on curve P-256. In the second case, both devices derive a shared secret, the FEK is wrapped with a key derived from it using AES-GCM, and only the wrapped blob is relayed. The server never learns the wrap key, so it never learns the FEK. The pairing code expires in 5 minutes and is rate-limited to 3 attempts.
Q: Can Filarr read my files if served a legal request? Not from what it stores. Filarr holds your key only wrapped, under your password and your recovery phrase. The server stores opaque blobs and an encrypted manifest it cannot open, so there is nothing readable to transmit. The limit is the one named above: during the transition, a server modified to capture the password of an account that still sends it could then unwrap that account's key. The flip side is that Filarr also cannot recover your data if you lose both your password and your recovery phrase.
Q: Can I use my own storage instead of Filarr's? Yes. Sync supports BYOS (bring your own storage) with any S3-compatible bucket, so the encrypted blobs land in storage you control while the same zero-knowledge properties hold.
Q: What metadata can the server still infer about me? The number of files, individual blob sizes, chunk counts, total storage used, and sync timestamps. It cannot infer filenames, folder names, or content. If hiding sizes and timing matters to you, note that this layer does not pad or obfuscate them.
If you want to check part of this yourself, the desktop client's code is public (BSL 1.1): it shows how your files are encrypted on disk. The three files quoted here — infra/cloudflare-worker/src/sync.ts for the server side, electron/sync/syncService.ts for the on-device orchestration, and electron/pairingService.ts for the key exchange — belong to the private cloud modules, not to the public repo.