Problem
A fresh app created with @vendure/create ships with dbConnectionOptions.synchronize: false and a migrations glob, but no migration files. As a result, the first deployment against a fresh/empty production database creates no tables — the schema is never built, and nothing in the flow explains why.
This is currently masked during local development because @vendure/create force-sets synchronize: true while populating the seed data, so the local schema is created by synchronize, not by a migration. The gap therefore only surfaces on the first real deployment, where nothing turns synchronize on.
The manual workaround — temporarily enabling synchronize: true for the first deploy, or hand-generating an initial migration against a throwaway empty database — is not something a first-time user would know to do, and synchronize: true is not acceptable for production.
Root cause
generateMigration() produces a migration by diffing the entity metadata against whatever database is currently connected (TypeORM's SchemaBuilder.log()):
- against an empty database → the diff is the full schema (exactly the "initial migration" we want);
- against a populated database → the diff is empty, so no migration can be generated.
So once the local database has been built (via synchronize), there is no straightforward way to bootstrap the initial migration — you'd need to point at an empty database of the same dialect.
Proposed solution
Two complementary changes:
1. Generate an initial migration at scaffold time (@vendure/create)
At scaffold time the configured database is genuinely empty, so a plain generateMigration({ name: 'init' }) yields the complete baseline. The scaffold then runs that migration to build the schema and populates with synchronize: false. Result: every new project ships with a schema-creating migration from day one, the first deployment builds its schema via migrations, and local dev matches production (no synchronize).
2. Add a shadow-database baseline mode to the CLI
vendure migrate --generate <name> --from-empty — generate a complete "zero to current schema" baseline even when the configured database is already populated, by diffing against a freshly-provisioned, disposable empty database of the same dialect (the approach used by Prisma's shadow DB and Atlas's dev DB). For Postgres/MySQL this creates and drops a temporary database on the same server; for SQLite it uses an in-memory database. This covers CI, regeneration, and scaffold paths where no empty database is available at generate time.
Prototype status
A working prototype exists for both parts (in @vendure/core, the CLI, and @vendure/create) and has been verified against Postgres 16 and better-sqlite3:
| Scenario |
Result |
--from-empty against an empty Postgres |
full baseline (87 CREATE TABLE + 260 ALTER TABLE) |
| plain generate against a populated DB |
"No changes found" — reproduces the bug |
--from-empty against a populated DB |
full baseline anyway |
| shadow DB lifecycle |
created and dropped, no leaks, target DB untouched |
| scaffold path (generate → runMigrations → populate) |
88 tables created and recorded in the migrations table |
SQLite --from-empty (in-memory) |
full baseline, real db file untouched |
Remaining work
Problem
A fresh app created with
@vendure/createships withdbConnectionOptions.synchronize: falseand amigrationsglob, but no migration files. As a result, the first deployment against a fresh/empty production database creates no tables — the schema is never built, and nothing in the flow explains why.This is currently masked during local development because
@vendure/createforce-setssynchronize: truewhile populating the seed data, so the local schema is created by synchronize, not by a migration. The gap therefore only surfaces on the first real deployment, where nothing turns synchronize on.The manual workaround — temporarily enabling
synchronize: truefor the first deploy, or hand-generating an initial migration against a throwaway empty database — is not something a first-time user would know to do, andsynchronize: trueis not acceptable for production.Root cause
generateMigration()produces a migration by diffing the entity metadata against whatever database is currently connected (TypeORM'sSchemaBuilder.log()):So once the local database has been built (via synchronize), there is no straightforward way to bootstrap the initial migration — you'd need to point at an empty database of the same dialect.
Proposed solution
Two complementary changes:
1. Generate an initial migration at scaffold time (
@vendure/create)At scaffold time the configured database is genuinely empty, so a plain
generateMigration({ name: 'init' })yields the complete baseline. The scaffold then runs that migration to build the schema and populates withsynchronize: false. Result: every new project ships with a schema-creating migration from day one, the first deployment builds its schema via migrations, and local dev matches production (nosynchronize).2. Add a shadow-database baseline mode to the CLI
vendure migrate --generate <name> --from-empty— generate a complete "zero to current schema" baseline even when the configured database is already populated, by diffing against a freshly-provisioned, disposable empty database of the same dialect (the approach used by Prisma's shadow DB and Atlas's dev DB). For Postgres/MySQL this creates and drops a temporary database on the same server; for SQLite it uses an in-memory database. This covers CI, regeneration, and scaffold paths where no empty database is available at generate time.Prototype status
A working prototype exists for both parts (in
@vendure/core, the CLI, and@vendure/create) and has been verified against Postgres 16 andbetter-sqlite3:--from-emptyagainst an empty PostgresCREATE TABLE+ 260ALTER TABLE)--from-emptyagainst a populated DBmigrationstable--from-empty(in-memory)Remaining work
@vendure/createchange (generate + runinitmigration, dropsynchronize: true)vendure migrate --generate <name> --from-empty(core shadow-DB provisioning + CLI flag)sql.js) by falling back to--from-emptyCREATE DATABASEprivilegeresetConfig()reloads the default config via a synchronousrequire()of an extensionless module path, sogenerateMigration()cannot run under the vitest/SWC runtime without a mock