ADR-0010: Versioned Data and Migrations¶
Status: Accepted
Date: 2026-02-27
Relates to: S7 from the improvement plan
Context¶
BasePlayerStore<TData> is the abstract base class for all per-player DataStore-backed stores. The TData generic had no constraint, which meant:
- Subclasses were not required to include a
__versionfield in their stored data. - There was no migration hook — schema changes to stored data would silently produce corrupt/partial objects when old data was loaded.
PlayerDataStore<T extends VersionedData>(the higher-level store) already handled versioning, but the simplerBasePlayerStorethat feature stores extend did not.
Decision¶
Opt-in versioning via schemaVersion()¶
The TData generic remains unconstrained so that simple stores work without modification. Versioning is opt-in: stores that need data migrations override schemaVersion() and include __version in their data.
export abstract class BasePlayerStore<
TData,
TConfig extends BaseStoreConfig = BaseStoreConfig,
>
schemaVersion() — non-abstract, defaults to 0¶
protected schemaVersion(): number {
return 0;
}
A return value of 0 means "no versioning" — the migration block in load() is skipped entirely. Stores that use versioning override this to return a positive integer.
Migration hook¶
A default-no-op migrate() method is provided:
protected migrate(data: TData, _fromVersion: number): TData {
return data;
}
Subclasses override this to transform old data into the new shape. The method receives the loaded data and the version it was stored at.
Version check on load¶
load() checks schemaVersion() first — if it returns 0, the entire migration block is skipped. When versioning is active (schemaVersion() > 0), it compares the stored version against the current one:
const currentVersion = this.schemaVersion();
if (currentVersion > 0) {
const storedVersion = data.__version ?? 0;
if (storedVersion < currentVersion) {
this.data = this.migrate(this.data, storedVersion);
this.data.__version = currentVersion;
this.markDirty();
}
}
Policy for feature stores that opt in¶
When a feature store wants data versioning:
- Include
__version: 1in default data (or extendVersionedData). - Override
schemaVersion()to return the current version number. - When any stored field is added, renamed, or removed, increment
schemaVersion(). - Implement
migrate(data, fromVersion)to transform old data — handle each version step.
Consequences¶
- Existing
BasePlayerStoresubclasses are not affected — they continue to work without any changes. - Stores that opt into versioning by overriding
schemaVersion()gain automatic migration on load. - Data saved with no
__versionfield is treated as version 0 and will trigger migration on first load if the store hasschemaVersion() > 0. - Migration only runs on load, not on save — saves always write the current schema version.