Build1 publisher3 min readPublished
Drizzle's generated SQL migrations keep the audit trail that push skips
One developer shipping Drizzle ORM on two SvelteKit kits keeps drizzle-kit push in prototyping because it writes no migration history. Production gets generated SQL that is read before it runs, plus a boot-time migrate that multiple replicas can race.
The Engineer · Build desk

What happened
- A developer with three SvelteKit starter kits runs schema-first Drizzle migrations in production on two of them, one on SQLite/D1 and one on Postgres.
- drizzle-kit push diffs the schema directly against a live database and applies whatever makes them match, writing no migration files and keeping no history.
- According to the post, two instances booting at once can both treat a migration as pending, ending in a column-already-exists error or a partially applied migration.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure Under push, a schema.ts that has fallen behind production turns into a delete instruction, dropping columns and indexes before anyone has read the change.
- constraint A team debugging a database changed by push has no last-applied migration to inspect and no ordered set of files to replay into a fresh environment.
- capability Backfills, partial indexes and data fixes can pass review in the same SQL file as the schema change that needs them.
- decision Multi-replica deployments have to make the pre-deploy script the single migration writer and let it finish before any replica boots.
The two commands compare against different things. `drizzle-kit generate` diffs schema.ts against the last snapshot Drizzle recorded and writes the difference to a SQL file [2]. `drizzle-kit push` diffs schema.ts against whatever live database it is pointed at and applies the difference on the spot [4]. The first produces a file before any database changes. The second produces a changed database and no record of what changed or when [5].
The author's three objections to push apply "once other people (or real data) depend on that database" [19]. The first is the missing audit trail [5]. The second is deletion. "push will happily drop columns, rename tables, and delete indexes that aren't in your schema anymore," the author wrote [6]. "In production, that deletes real data without a review step." [7] The third is recovery. Generated migrations are plain SQL, so a backfill, a partial index or a data fix can go in before or after the generated statements [8]. After a bad push, the post says, the only tool left is writing manual SQL from scratch [9].
I think the rule is right for any Drizzle database holding data someone would miss. The evidence is one developer's experience across two kits [1], and the post reports no incident counts. The argument still carries over to other projects, because it depends on what each command diffs against, and that does not vary by project. The author's complaint is that the Drizzle docs "don't clearly tell you when each one is the right call" [20].
The post's workflow has four steps [11]:
1. Edit schema.ts. 2. Run `npx drizzle-kit generate`. 3. Read the generated SQL. 4. Run `npx drizzle-kit migrate`.
The author says step three is where typos get caught [11]. `migrate` then applies pending files in order and records what ran [3]. "The migration history becomes the source of truth for how the database evolved," the author wrote [21].
One line in the shared `drizzle.config.ts` needs care in CI. The connection string is `process.env.DATABASE_URL || 'postgres://user:pass@localhost:5433/db'`, a fallback added so `docker compose up` works with no setup [10]. In a pipeline where the variable is missing, drizzle-kit is sent to localhost:5433 instead of stopping on the unset variable [17].
The Postgres kit adds a second apply path. `createDb()` calls `migrate(db, { migrationsFolder: MIGRATIONS_DIR })` at boot, and the deploy docs still list `npm run db:migrate` as an explicit step [12]. The author's reasoning is that the explicit step makes a deploy a reviewed, ordered event. The boot call guarantees no instance serves traffic against an unmigrated schema, which matters most for preview deploys and replicas that came up unexpectedly [13]. Because the call sits in `createDb()` and not in `hooks.server.ts`, scripts and tests are migrated too [14].
The boot call has a failure mode, and the post describes it. Two instances booting together can both decide a migration is pending, and the result is `column already exists` or a partially applied migration [15]. The author's rule for multiple replicas is to keep the pre-deploy step as the only writer and treat boot-migration as a no-op [16]. That holds because `migrate` applies only pending files [3]. If the pre-deploy step has already applied everything, each booting instance finds nothing to do, provided that step finishes before the first replica starts [18].
What to watch
- Whether Drizzle's documentation adds explicit guidance on when push versus generate is appropriate, the gap the author complains about.
- How the D1 kit applies migrations through Cloudflare's platform tooling in place of drizzle-kit migrate.
- Reports of how the boot-time migrate call behaves on Postgres when several replicas start at once, beyond the two failure modes the post lists.