FireUX · 4 min
A Clean TypeScript Model Is Not a Data Migration
Why changing an Entity type is only the first gate in a safe data migration, and what must align before legacy authority can be retired.

Cleaning an Entity model feels decisive. Duplicate fields disappear, optional properties become explicit and the TypeScript type finally describes the record we want.
But a clean type does not prove that the system has migrated.
Readers may still accept the old shape. Writers may continue producing it. Database documents may contain duplicate authority. A projection may quietly stand in for the Entity. Even a passing guard can preserve the mechanism it was supposed to retire.
We recently formalized a stricter lesson at FireUX: an Entity is aligned only when its model, consumers, authority, stored data, projections and product surfaces agree on one ownership tree.
The partial-migration loop
A common migration begins by updating a TypeScript interface. Compatibility fields are then added to keep existing pages working. Readers fall back from the new name to the old name, and writers update both. A guard confirms that all expected fields are still present.
The code compiles, but the old system is now protected by the migration:
- A legacy consumer needs duplicate data.
- The migration preserves that duplicate.
- A dual writer keeps both versions current.
- A guard requires the legacy consumer or field.
- The old hierarchy starts to look canonical again.
This is why compatibility can be more dangerous than an obvious failure. A broken reader forces a decision. A fallback can postpone that decision until nobody knows which value is authoritative.
Use gates, not a broad rewrite
Our current hard-cutover sequence separates the migration into eight gates.
1. Define the canonical model
Create one plain singular type for the root document. Keep infrastructure, related Entities, operational children, presentation and derived facts outside it. Document identity should come from one declared source.
2. Align every consumer
Find readers, writers, normalizers, commands, responses, fixtures and pages. Remove fallback field names, replacement DTOs that masquerade as the Entity and broad Partial<Entity> authority.
3. Establish authority
Name one owner for identity and one explicit access path. A surface such as a Workspace, Directory or Detail page presents an Entity; it does not become a second storage owner.
4. Audit stored data
Inspect each relevant database. Classify missing canonical fields, fields outside the model, duplicate roots and misplaced child records. Source code cannot tell you which historical shapes still exist in storage.
5. Run a protected migration
Retain an exact preimage or export. Produce a deterministic plan and hash it. Apply through a guarded mutation lane, use version preconditions where appropriate and verify both values and database types after writing.
6. Retire the old shape
Delete superseded fields or documents only after their consumers have moved. Then remove old readers, writers, aliases, mutation targets and fallbacks. Dormant compatibility code is still an invitation for the old shape to return.
7. Prove the product surfaces
Exercise the Directory, Detail and authorized Workspace from the remaining canonical data. Debug output should separate canonical objects from derived relationships, routes, counts and presentation.
8. Make guards reject regression
The final guards should reject the retired fields, paths and parallel authority. Focused checks provide iteration feedback; the complete repository guard proves that the cutover did not damage another family.
Projections are not automatically wrong
A bounded projection may still be useful for a public route, search result or joined server response. The test is whether a current product or transport requirement independently needs it.
A legitimate projection has an explicit name, derives from validated canonical authority and contains only the fields its consumer needs. It is never accepted as the canonical Entity or as access authority.
The same applies to child records. Team membership, finance facts and operational records may live beneath their owning Entity without appearing on the root type. The problem is not hierarchy itself; the problem is duplicated identity or lifecycle ownership.
Evidence required at handoff
“Migration complete” should be an evidence statement, not a feeling. Record:
- the source files and databases inspected;
- the drift found at each gate;
- the exact records and files changed;
- the plan hash and retained recovery evidence;
- post-write verification, including stored value types;
- focused checks and the complete guard result;
- every remaining difference classified as accepted, drift or unresolved.
Do not use “migration” as a permanent completion state. If the product still requires the old authority path, the Entity is not aligned yet.
What we now do at FireUX
FireUX runs canonical Entity work as a bounded hard cutover. We move one family through the gates rather than mechanically changing every model at once. Firestore mutations use typed Client, Server or MCP/Skill lanes; scripts and AI agents do not receive separate raw database authority.
This complements our approach to keeping public pages away from raw database records and extending the Nuxt spine beneath real Entity owners.
Future tasks may use this article as orientation, but it is not implementation authority. They must inspect current source, guards, living context and verified database evidence before changing an Entity.
The lasting lesson is simple: a type describes the intended record. A migration is complete only when the entire running system has stopped needing the record it replaced.