A zero-knowledge service puts a specific responsibility on the user: if you lose the password, the data is gone. Backups are the insurance policy. Flowvault's answer is the .fvault file: exactly the ciphertext the server already holds, plus the KDF parameters needed to unlock it, bundled into a single JSON envelope that stays zero-knowledge even on your desktop. This post walks through the format, the design decisions, and how it compares to the exports from Bitwarden, Standard Notes, and CryptPad.
What a zero-knowledge backup has to do
There are four constraints, and they trade off in awkward ways:
- Must be enough to restore without the operator. The whole point of the backup is that the service could vanish tomorrow and you'd still be fine. That means salt, KDF parameters, volume layout, and ciphertext all have to be in the file.
- Must stay zero-knowledge once downloaded. Your password can't be in the file. Neither can any plaintext. A stolen backup must be indistinguishable from a stolen Firestore document.
- Must preserve deniability.If you have decoy notebooks, the backup has to round-trip them too — without ever listing how many active slots there are.
- Must be restorable on any instance. Not just ours. A self-hosted Flowvault (your own Firebase project) should be able to read the file and recreate the vault.
The .fvault format is engineered backwards from those four constraints.
The shape of the file
A .fvaultis a single UTF-8 JSON document. Binary fields are Base64url-encoded because JSON doesn't handle raw bytes. The envelope looks like this (with a real-world example truncated):
{
"kind": "flowvault-backup",
"version": 1,
"exportedAt": 1745272800000,
"slugHint": "project-alpha",
"kdfSalt": "C0X7...base64url...",
"kdfParams": {
"algo": "argon2id",
"memoryKiB": 65536,
"iterations": 3,
"parallelism": 1
},
"volume": {
"format": "hidden-volume-v1",
"slotCount": 64,
"slotSize": 4096
},
"ciphertext": "RVaB...base64url (256 KiB of slots) ..."
}Every field is there for a specific reason. Let's walk through them.
kind + version
Magic string and format version. On open, the client asserts kind === "flowvault-backup" and checks the version. The current format is v1; anything newer gets handled when it ships, older gets a migration path. The magic string is there so a mis-uploaded file (a ZIP, a PDF, a wrong.fvault from a different tool) fails fast with a clear error rather than trying to decrypt random bytes.
exportedAt
Unix milliseconds. Just metadata. Not used for decryption, but useful if you have multiple backups and want to pick the newest.
slugHint
The URL slug the vault was originally at. This is not authoritative— on restore, you choose a fresh slug (the old one might be taken, or you might be restoring into a private self-hosted instance with different slug rules). It's there so the restore page can pre-populate the field with a sensible default.
kdfSalt + kdfParams
The Argon2id parameters that turn your password into a key. The salt is 16 random bytes that were generated when the vault was first created; it never changes across saves, so a backup and the live vault share the same salt. The parameters are frozen per-vault: if we increase defaults in the future, old vaults keep their old parameters (or get migrated on your explicit opt-in).
volume
The hidden-volume layout: format version, slot count (64), slot size (4 KiB by default). Also frozen per-vault. Future volume formats will add a new format string and migrate alongside a version bump.
ciphertext
The main payload. It's exactly slotCount * slotSize = 256 KiB of bytes, Base64url- encoded (which makes it roughly 342 KiB on disk). This is the same blob the server has, copied byte-for-byte.
Indistinguishable from random. Decoy slots are random. Active slots are AES-GCM ciphertexts keyed to your password(s). No structure distinguishes the two.
“Zero-knowledge even on your laptop”
The crucial property: once downloaded, the file tells an attacker the same amount about your notes as the Firestore blob tells our server — which is to say, nothing. The KDF parameters are public info (same for all v1 vaults). The salt is a random 16 bytes; it doesn't leak anything useful. The ciphertext is AES-GCM under Argon2id-derived keys; without a password it's noise.
Consequences:
- Emailing yourself a
.fvaultover Gmail: the ciphertext is still only readable with your password. Gmail is not in your threat model for the contents. - Dropping one on a USB stick and losing the stick: ditto.
- Storing in iCloud / OneDrive / Dropbox: provider sees 350 KB of random-looking bytes per backup. They can't tell what you wrote.
The restore flow
To restore, you go to /restore and drop the file. The client:
- Parses the JSON envelope. Checks
kindandversion. Decodes the Base64url fields. - Lets you pick a fresh slug. (Existing slugs are rejected — Flowvault refuses to overwrite a live vault.)
- Uploads the ciphertext + KDF salt + KDF params + volume params as the initial document for the new slug.
Restore neverasks for a password. There's no decryption happening. The whole restore operation is “create a new Firestore document pre-populated with the ciphertext.” After restore, open the new URL normally and enter your original password(s) to read.
This matters for self-hosting too: if you run your own Flowvault instance (see the self-host guide in the README), you can restore backups made against our hosted service into your own infrastructure. The ciphertext is portable.
Plaintext Markdown export
Sometimes you genuinely want plaintext. Moving to Obsidian, committing notes to a private git repo, sharing with a collaborator who doesn't have a Flowvault URL. The Export menu in the editor toolbar has a second option: Plaintext Markdown (.zip).
Rules:
- Behind an explicit confirmation (“this creates unencrypted files on your disk”).
- Only the current slot is exported.If you have decoy notebooks behind other passwords, they're not touched — preserving deniability even if somebody asks “can I see the whole export?”
- Each tab becomes a
.mdfile named after the tab; aREADME.mdin the root of the zip lists them. - The zip uses the minimal STORE method (no compression), which makes the implementation small and dependency-free — Flowvault ships its own 400-line zip writer rather than pulling in a full archiver.
Compared to other encrypted-notes exports
Bitwarden vault export
Bitwarden exports both unencryptedJSON and an encrypted variant. The encrypted variant is password-protected but uses a user-chosen password that's independent of the vault's master password, so it's effectively a second account of trust. Importing into a different Bitwarden instance works; importing into any other product doesn't (format is bespoke). Decoy notebooks don't apply — Bitwarden has no deniability concept.
Standard Notes backups
Standard Notes has automated encrypted backups: scheduled exports to disk or cloud, encrypted with a user-provided password. Format is proprietary but documented. Same pattern as Flowvault: self-contained, zero-knowledge, restorable. The main differences are UI polish (scheduled vs manual) and format complexity (Standard Notes's export is more featureful because notes are typed items, not raw text).
CryptPad exports
CryptPad exports individual pads as unencrypted Markdown, a sharable “hidden link” (which includes the key in the fragment), or the raw CryptPad drive dump. There's no single-file encrypted backup format equivalent to .fvault. If you want portable encrypted snapshots of a whole CryptPad drive, you're largely relying on the server's presence.
ProtectedText exports
ProtectedText has no export function. The only way to get a copy is manually copy the visible text, which is lossy and manual. This was one of the specific gaps that motivated adding .fvault + Markdown export to Flowvault.
Flowvault Bitwarden Standard CryptPad Protected-
(.fvault) encrypted Notes (raw) Text
JSON backup
Single-file encrypted export yes yes yes no no
Zero-knowledge on disk yes yes yes partial n/a
Plaintext export available yes yes yes yes manual only
Preserves decoy volumes yes n/a n/a n/a n/a
Restorable without account yes yes yes limited no
Self-host compatible yes yes yes yes noPractical advice
- Back up whenever you make significant edits. A
.fvaultis 350 KB; it costs nothing to keep a dozen timestamped copies. - Store backups where a future you will find them. A cloud drive is fine because the file is still zero- knowledge; an encrypted USB is fine; emailing to yourself is fine.
- Pair with the trusted handover. A handover ensures your beneficiary can read the live vault; a
.fvaultensures they can read it even if Flowvault itself has stopped existing. - Test a restore at least once. Download a backup, restore it to a fresh slug, unlock, verify the tabs match. Do this before you ever actually need it.
- Use Markdown export only when you mean it. Once plaintext
.mdfiles are on your disk they travel with your backups, your Dropbox, and your recycle bin. That's fine for migration; bad for routine snapshots.
Format evolution
.fvault is versioned for a reason. Likely future additions:
version: 2— larger slot size for bigger notebooks, breaking change for the on-disk layout.- Optional post-quantum KDF parameters, once the ecosystem settles on a scheme.
- Multi-file snapshot bundles for very large vaults (chunked ciphertext).
Backwards compatibility is the constraint: restore must always accept any old version we've ever shipped. That's why the envelope carries its full KDF and volume parameters rather than reading them from the current server defaults.
See also
/restore— the restore page, with a file picker that accepts.fvault.- The beginner's guide — where backup / restore fits in the broader workflow.
- Hidden volumes explained — why decoy slots stay indistinguishable even in a backup file.
- Trusted handover — complementary: handover for live vaults,
.fvaultfor offline insurance. - The security page— exact parameter values and format references.