Data migrations
Extensions own the versions and transformations of their stored data. Core provides these public helpers on ctx.storage.
Run a migration
await ctx.storage.migrate(2, async (previousVersion) => {
if (previousVersion < 1) {
const legacy = await ctx.storage.readLegacy("old_notes");
for (const record of legacy?.records ?? []) {
if (typeof record.id !== "string") continue;
await ctx.storage.collection("notes").createIfAbsent(record.id, record);
}
}
if (previousVersion < 2) {
// Transform this extension's version-one records here.
}
});
The version must be a positive safe integer. Each extension instance and storage scope has its own checkpoint. Concurrent calls serialize. An equal version skips the callback; a newer stored version reports an error instead of running older code. Only a successful callback advances the checkpoint.
Call the helper before accessing the extension’s data. Use the same migration entry point from each command or hook that needs that data. Other extensions and core navigation remain available.
The helper does not roll back callback writes. Make each step safe to retry and preserve newer user data with conditional creation. Do not call migrate recursively on the same storage scope.
Read retained source records
readLegacy(source) returns { version, records } or undefined. The version names the schema immediately before the table was removed. Records keep their original column names and JSON values. Your extension validates and interprets them.
The host reads only retained tables and filters records to the invoking project. A record with an existing extension instance owner is visible only to that instance. This API cannot read active tables, global records, or another project’s data.
The host retains the source after import and failure. Reading it does not acknowledge, delete, or transfer it. Existing project file IDs can be read with ctx.files.readText; imported content uses the ordinary extension attachment API.
Test migration code
createMemoryStorage({ legacyData: { old_notes: { version: "old-schema", records: [...] } } }) from @pstdio/sdk/testing supplies retained-data fixtures. It implements scope-local migration versions and returns copies of source records. Test first import, retry after a partial write, newer-data preservation, and repeated calls.
See ADR 0077 for storage ownership and retention.