How framework-owned tables in packages/core declare and change their schema with defineStore(). Use before adding a table, column, index, backfill or seed to a core store, before writing an ensure*Table() function, and when guard:release-schema-complete or SchemaNotMigratedError fires.
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Every framework table in packages/core/src is owned by exactly one
defineStore() from src/db/store-registry.ts. The release step applies a
store's migrations once per database and records each in
_an_store_migrations, then verifies the ledger covers every registered store.
Hosted functions of release-owned apps do no schema work at all. Design and
rollout:
docs/plans/2026-09-28-store-registry-schema-ownership.md.
App-owned template tables are not covered here; they use runMigrations or
Drizzle (see storing-data).
let _initPromise memo, no ensureTableExists / ensureColumnExists /
ensureIndexExists and no CREATE/ALTER outside a defineStore()
migration. Keep the module's exported ensure*() as
return xStore.ready(); so callers do not change.store-migrations.lock.json makes this a CI failure.CREATE TABLE IF NOT EXISTS plus ensureColumnExists for every column ever added. Never squash a
baseline into today's CREATE TABLE.after: ["other_store_id"] on its store; the release pass fails on an
unknown id or a cycle.runMigrations list. Append it whole with
...fromMigrationEntries(TABLE, LIST, { data: [...] }) to the store that
owns its tables. Each entry keeps its old identity, so databases that
applied it import it instead of re-running it. A release-only list keeps
running only in the release step; pass { ranAtBoot: true } for a list that
also ran at plugin boot, so local runtimes import its history and hosted
apps without release migrations keep its old bookkeeping. Never rename or
reorder the entries.kind: "data"; a schema
snapshot cannot replace them.isMigrationAuthorizedRuntime()), as widenIntColumnsToBigInt does.releaseOnly: true for a store a request must never create, even in
hosted apps that do not migrate at release.export const xStore = defineStore({ id: "snake_case_id", migrations: [...] }); the generator only reads that form,
and the guard fails on any other. Ids are permanent.export const widgetsStore = defineStore({
id: "widgets",
migrations: [
{
name: "baseline",
run: async () => {
await ensureTableExists("widgets", CREATE_WIDGETS_SQL);
await ensureColumnExists("widgets", "owner", `ALTER TABLE widgets ADD COLUMN IF NOT EXISTS owner TEXT`);
},
},
{ name: "owner-index", sql: `CREATE INDEX IF NOT EXISTS widgets_owner_idx ON widgets (owner)` },
{ name: "backfill-owner", kind: "data", sql: `UPDATE widgets SET owner = created_by WHERE owner IS NULL` },
],
});
export function ensureWidgetsTable(): Promise<void> {
return widgetsStore.ready();
}Then run, in this order:
pnpm gen:store-registry — rewrites store-registry.generated.ts and
store-migrations.lock.json. Then pnpm gen:schema-snapshot — rewrites
schema-snapshot.generated.ts, the schema fresh databases install in one
step. Commit all three; release-schema.snapshot.spec.ts fails when the
snapshot no longer covers every schema migration.pnpm guard:release-schema-complete and pnpm guard:additive-migrations.src/db/store-registry.spec.ts,
src/server/release-schema.cold-start.spec.ts and
src/server/release-schema.upgrade.spec.ts from packages/core. The
upgrade spec replays an older release's schema and fails when your change
does not reach existing databases (for example a column added only to a
CREATE TABLE).ready() picks a mode from who owns the schema:
withMigrationRuntime, under a lease in
_an_store_migration_lock, verifying the ledger against the registry
afterwards), plus local dev, tests and self-hosted Node. Each migration runs
once per database. Specs that mock getDbExec answer ledger statements with
db/store-ledger.test-helpers.ts.SchemaNotMigratedError is raised by the release step when the ledger does
not cover the code being shipped. It fails that deploy; fix the migration or
the deploy, never catch it. Code that catches everything around a store query
must fail closed for security reads (revocation, budgets, access): a missing
table must never read as allowed or empty.
// guard:allow-unreleased-schema — <reason> exempts a module from schema
placement checks. Current exceptions: sharing/indexes.ts (indexes app and
framework share tables at release) and triggers/event-queue.ts (owned by a
versioned list until those fold into stores). A new one needs a reason a
reviewer can check.
BuilderIO/agent-native
a0356c6
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.