An export button is not portability
Every app has an export. Almost none of them let you leave. The difference is whether the file is readable without the software that wrote it - and there is a short test that settles it.
“Your data is yours” is now table stakes. Every storage library says it, every privacy page repeats it, and the usual evidence is an export button somewhere in the settings. The button is real. It downloads a file. And in a large number of apps it still leaves you exactly as stuck as you were before.
The reason is that portability is a property of the file, not a feature of the app. An export produces bytes; whether those bytes are worth anything a year later depends on questions the button does not answer. Local-first architecture makes the question sharper rather than softer: the file is now the durable artifact, so if it is unreadable without the vendor, moving storage onto the user’s disk has changed who holds the data without changing who controls it.
The four questions
Take any export from any tool and ask these in order. Each one has failed real software.
Can it be read without the software that wrote it? A dump of the app’s internal database is not an export, it is a backup. If restoring it requires that application, at that version, then the file is a hostage rather than a copy. This is the test that eliminates most contenders immediately: an SQLite file, an IndexedDB dump, a proprietary blob with a documented extension and an undocumented interior.
Does the file say what it is? Open it in ten years, on a machine that has never run the app, and it should be possible to work out what you are looking at from the file alone: what wrote it, in which version, in which shape. A format that only makes sense to code that already knows the answer is one lost repository away from being noise. Self-description is why a schema number belongs inside the archive rather than in the app that reads it, which is the same argument that makes read-time migrations work at all.
Is it complete? Exports that quietly drop attachments, or the parts of the model considered “internal”, produce the worst outcome available: a file that looks like the whole thing and is not, discovered only during the import that was supposed to be the escape route. Binary files and relationships are where this fails, because they are the parts that are inconvenient to serialise.
Has anyone other than the author ever read it? This is the question people skip, and it is the one that decides the others. A format with exactly one implementation is a description of that implementation, no matter what the document claims. Nothing forces the two to agree until something outside the codebase tries to parse a real file and fails.
Why a specification is not enough on its own
Writing the format down is necessary and it is not sufficient, for a reason worth being blunt about: prose drifts from code silently, and always in the same direction. The implementation gains a field, a default changes, an edge case gets handled - and the document, which nobody executes, stays as it was. Six months later there are two formats with one name.
What closes the gap is a second reader in another language, run on every change, against pinned files. It is not there for people who want to write their own tooling, although they benefit. It is there as a disagreement detector: when the library and the document diverge, one of them fails the vectors and CI names which. The selfstore backup format is checked this way - the spec sits in the library repository, a roughly 120-line Python reader opens real backups with no JavaScript involved, and canonical vectors are committed next to it. A file written by the first release still reads today, and that is proven on each run rather than asserted in a release note.
Pinned vectors matter more than they look. They are the only mechanism that catches a compatibility break introduced by a refactor which passed every unit test, because the unit tests were rewritten alongside the code and the vectors were not.
Encryption does not exempt a format
An encrypted archive can still be self-describing, and it should be. The cleartext header names the format generation, the app and the schema version; the crypto parameters travel with the payload so a file written under older defaults keeps decrypting after the defaults move. What must not happen is a container whose interior can only be reconstructed by the vendor’s code path: that is lock-in wearing a security argument, and the two are easy to confuse because both produce an opaque file. The distinction is whether the opacity is removable with the key the user already holds. More on what that encryption does and does not buy in encrypted backups in the browser.
The test that settles it
One exercise, half an hour, and it is the only evidence that counts:
- Export a real file with real content, not a fixture.
- Move it to a machine that has never run the application.
- Open it with something that is not the application - a ZIP tool, a script you wrote from the specification, a reader in a different language.
- Check that everything is there, including attachments.
An app that survives this cannot lock you in, whatever its licence, its funding or its future. An app that fails it has not given you your data; it has given you a file it is still the only reader of. Nothing in the marketing distinguishes the two, which is precisely why the test is worth running before you have five years of content inside.
The uncomfortable consequence
A format you can leave is a format a competitor can import. That is not a side effect to be minimised, it is the point: the guarantee is only credible when leaving is genuinely cheap, and it is only genuinely cheap when somebody else could plausibly do it. Anything short of that is a promise, and a promise is exactly what portability is supposed to replace.
The upside is not altruism either. Software that can be left has to stay worth using, which is a healthier discipline than the alternative - and users who know they can leave are the ones who commit their real data to it in the first place.