Available for hire — privacy-first apps, engineering, and business-idea work.contact@flowdesk.tech
Flowvault
· 9 min readfeatureformat

The .fvault format: zero-knowledge backups for an encrypted notepad

How to snapshot every slot, every decoy password, and every tab — without ever handing the server a plaintext byte. Plus a plaintext Markdown export for migrating out.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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):

flowvault-backup (v1)
{
  "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 .fvault over 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:

  1. Parses the JSON envelope. Checks kind and version. Decodes the Base64url fields.
  2. Lets you pick a fresh slug. (Existing slugs are rejected — Flowvault refuses to overwrite a live vault.)
  3. 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 .md file named after the tab; a README.md in 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.

Export feature comparison
                              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       no

Practical advice

  • Back up whenever you make significant edits. A .fvault is 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 .fvault ensures 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

Related posts

Ready to try what you just read about?

Open a URL on the home page (no sign-up), send a self-destructing encrypted note, or seal a message for a future date at /timelock/new.