It is easy to assume that an application backed by Markdown files avoids the
hard parts of data migration. There is no database schema to alter, no foreign
key constraint to update, and no migration framework to run.
That assumption stops working as soon as filenames, metadata, links, and assets
begin carrying identity.
I encountered this while improving a Vue-based manager for a local card
collection. Each saved card is a Markdown note that can link to other notes and
reference a local image. Older records predated the current identity rules, so
an audit found notes with missing IDs, duplicate IDs, and filenames that no
longer matched their saved card printing.
Why changing one property was not enough
The first repair step looked small: select the correct printing and save its
stable identifier. But that identifier affected several parts of the system:
- the expected Markdown filename
- the image filename and
Coverproperty - links written in other Markdown notes
- duplicate identity checks
- filename collision checks
- cleanup of the previous image
Updating only the front matter would leave the collection internally
inconsistent. Renaming only the note could break Obsidian links. Deleting the
old image could break another note if the asset was shared.
The operation was not a field edit. It was a migration.
Treating the filesystem as a data model
The repair flow became an explicit sequence:
- Validate the requested identity.
- Reject duplicate identities and target filename collisions.
- Rename the note to its stable filename.
- Rewrite Markdown and wiki links that reference the previous name.
- Update the card image and
Covermetadata. - Remove obsolete assets only after confirming they are unused.
- Run the collection audit again.
The ordering matters. A failed rename should not leave references pointing to a
file that never moved. Cleanup should happen after the new state is valid, not
before it. Collision checks should fail safely instead of guessing which file
is authoritative.
This is similar to a database migration even though the storage mechanism is
different. The invariants still need to be explicit:
- one stable identity belongs to one saved card
- a stable filename must not overwrite another note
- references must continue resolving after a rename
- a shared asset must not be removed while another note still uses it
Verification beyond the happy path
The backend route tests cover validation, collision handling, link rewriting,
and repair behavior. A complete browser lifecycle test exercises the feature
through the real application flow.
After the repair work was completed, the collection was normalized and the
audit reported no remaining identity or filename issues.
The most important verification was not that a card could be repaired once. It
was that the operation failed safely when the target identity or filename was
already in use, and that rerunning the audit produced a stable result.
What I learned
Local-first storage changes the infrastructure, not the need for integrity.
In a relational database, relationships are visible in tables, keys, and
constraints. In a filesystem-backed application, they may be distributed
across filenames, front matter, links, configuration, and shared assets. That
makes them easier to overlook, not less important.
The practical rule I am keeping is:
If changing a value can invalidate references elsewhere, the operation should
be designed and verified as a migration.
Plain files are a powerful source of truth, but they still deserve explicit
identity rules, safe failure modes, and repeatable audits.
Top comments (0)