Appearance
Migration and Changes
This guide explains how migration and change management work in Vona within the Cabloy monorepo.
Why this system exists
Vona provides a migration mechanism designed for long-lived modular projects.
Several important characteristics define this migration system:
- module-aware migration
- multi-tenant initialization support
- test-data support
- production-safe execution
- full access to the Vona ecosystem inside migration code
This makes migration in Vona more than a one-off schema script. It is part of the framework’s modular lifecycle.
Migration in the backend contract loop
A useful backend-contract-loop lifecycle mental model is:
- entity and model define the current structural contract
- DTOs and controllers define the API contract on top of that structure
meta.versionmanages schema evolution and initialization data- tests verify that the migrated backend still satisfies the intended contract
That is why migration should be understood as part of the same backend thread, not as a separate maintenance topic.
Define data version
Each module declares its current data version in its own package.json.
Representative pattern:
json
{
"name": "vona-module-training-student",
"vonaModule": {
"fileVersion": 1
}
}The key rule is:
- increment
fileVersionwhen a released module introduces a new schema change that must be applied in sequence
In the scaffolded CRUD workflow, this is not an isolated maintenance step. The generator-driven thread treats fileVersion as part of the same backend evolution path that also touches entity/model/controller/test resources.
Decide whether to increment fileVersion
For a new persisted field or another schema-shape change on an existing resource, decide whether vonaModule.fileVersion should change before editing meta.version.ts, a versioned schema path, or the module package.json.
Increment fileVersion
Choose a new migration version when the change must be applied sequentially to an existing released module:
- increment
fileVersion; - add a new migration branch;
- preserve older branches as historical snapshots;
- introduce the schema change in the new branch.
Do not introduce the same column in an older create path and again in a later migration branch. A fresh installation can execute version branches sequentially and fail on a duplicate column.
Keep the current fileVersion
When the change belongs in the current version path rather than new migration history:
- keep the existing
fileVersion; - fold the schema change into the current version path;
- do not create a new migration branch.
Ask before choosing this strategy; do not infer migration history from the field change alone. Any change to meta.version.ts requires npm run test so the test database is recreated and schema/data consistency is exercised.
meta.version
Vona uses a bean named meta.version to organize migration code for a module.
Create it with:
bash
npm run vona :create:bean meta version -- --module=training-studentRepresentative shell:
typescript
@Meta()
export class MetaVersion extends BeanBase {}Three change scenarios
Three migration scenarios are defined:
| Scenario | Purpose |
|---|---|
update | schema evolution such as tables and fields |
init | instance- or tenant-specific initialization data |
seed | baseline data for test runs and explicit dev resets |
This split is one of the most important Vona migration ideas because it separates:
- structural change
- initialization logic
- shared baseline data setup
A practical generated-thread interpretation is:
updatefollows schema and entity/model evolutioninitfollows instance-aware initialization needs introduced by the backend featureseedprovides shared baseline data during test-mode startup or an explicit development database reset
Update: schema migration
Representative pattern:
typescript
@Meta()
export class MetaVersion extends BeanBase implements IMetaVersionUpdate {
async update(options: IMetaVersionUpdateOptions) {
if (options.version === 1) {
await this.bean.model.createTable('trainingStudent', table => {
table.basicFields();
table.string('name', 50);
table.string('description', 255);
});
}
}
}A typed style based on entity metadata is also supported, and it is usually the better long-term default when possible.
Init: tenant-aware initialization
Representative pattern:
typescript
@Meta()
export class MetaVersion extends BeanBase implements IMetaVersionInit {
async init(options: IMetaVersionInitOptions) {
if (options.version === 1) {
await this.scope.model.student.insert({
name: 'Tom',
description: 'This is a student',
});
}
}
}The important point is that initialization can run per instance or tenant, which keeps tenant data isolated.
Seed: test and reset baseline data
Representative pattern:
typescript
@Meta()
export class MetaVersion extends BeanBase implements IMetaVersionSeed {
async seed() {
await this.scope.model.student.insert({
name: 'Jimmy',
description: 'Shared test-environment baseline data',
});
}
}This is valuable because shared baseline data becomes part of the structured version lifecycle instead of being scattered across unrelated setup code.
Vona invokes seed() while the application starts in test mode (npm run test or managed E2E), and during an explicit npm run db:reset in dev mode. Ordinary development startup and framework upgrades do not seed or reset development data automatically. Use seed() for durable baseline fixtures shared by tests or for freshly prepared local development data. Both paths seed after instance initialization on a newly recreated disposable database; seed() is not a repeatable, same-database data-import mechanism.
Treat shared seed() records as read-only in ordinary tests. A scenario that needs different state must create independent, test-owned data and clean it up in finally; it must not mutate or delete the shared baseline. seed() remains distinct from init(): use init() for instance-aware initialization data and reserve seed() for baseline data in test runs or explicit development resets.
Version changes across the generated backend thread
When the generated CRUD thread evolves, migration should evolve with it.
A practical sequence is:
- decide whether to increment
fileVersion - add or adjust entity/model structure
- update the current or new
meta.versionpath to match that decision - rerun migration locally
- verify the contract through tests and controller actions
This keeps schema change, backend contract change, and verification tightly connected.
Local development workflow
One important distinction for local development is:
- when iterating locally, you often want to recreate the database and re-run migration logic without bumping
fileVersion
Representative commands:
bash
npm run test
npm run db:resetA practical decision rule is:
- use
db:resetto recreate the disposable dev-mode databases, rerun migrations and instance initialization, and then invoke moduleseed()hooks; it deletes existing matching development databases, so do not run it against data you need to keep db:resetrequiresdatabase.testDatabase: truein dev config and a connection outside the disposable database; it refuses to reset a configured non-disposable database- use
testwhen you need test-mode verification of migration, controller behavior, and the broader generated contract thread; E2E also remains test-mode
Implementation checks for migration-sensitive changes
When changing backend schema or module initialization behavior, do not only edit entities and models.
Also ask:
- does this change require a
fileVersionincrement? - does
meta.versionneed anupdate,init, orseedbranch? - should the local verification path include
testordb:reset? - does the change affect the CRUD-generated thread or frontend-facing API contract as well?
That prevents schema changes from being documented in code but never integrated into the module lifecycle.