Heximon Logo
CLI

Migrations from the CLI

heximon migrations generate/migrate/status/push — DatabaseConfigDetector auto-detects your DrizzleXConfig files, DotenvCascade loads .env, and DrizzleKitRunner drives .bin-resolved drizzle-kit with no hand-written drizzle.config.ts.

Your database config is already a valid drizzle-kit ConfigDrizzleLibSQLConfig and its pg/mysql2 twins carry dialect / schema / out / dbCredentials as own fields. heximon migrations turns that fact into a CLI: it finds your config file(s) on disk, loads your .env cascade, and drives the stock drizzle-kit binary for you — no drizzle.config.ts to hand-write, no --config flag to remember.

terminal
pnpx heximon migrations generate
pnpx heximon migrations migrate

How detection works

DatabaseConfigDetector walks the root drizzle.config.* plus every *.config.* under src/, database/, and app/, loads each candidate with jiti, and keeps the ones whose default export duck-types as a drizzle-kit config — a dialect field matching a real drizzle-kit dialect (sqlite, postgresql, mysql, turso, …). Your config just needs a default export:

src/database/database.config.ts
export const databaseConfig = new DrizzleLibSQLConfig(schema, relations, {
  dialect: "sqlite",
  schema: "./src/database/schema.ts",
  out: "./migrations",
  get url(): string {
    return Platform.get("TURSO_DATABASE_URL") ?? "file::memory:?cache=shared";
  },
  get authToken(): string | undefined {
    return Platform.get("TURSO_AUTH_TOKEN");
  },
  busyTimeoutMs: 5000,
});

export default databaseConfig;

The loaded driver also self-describes how a database applies, so migrate never needs you to name the dialect: a d1-http driver applies through wrangler d1 migrations apply, not drizzle-kit; a durable-sqlite driver bundles its migration in-DO with no CLI apply step at all; anything else runs through plain drizzle-kit migrate.

Pick which database

An app with one detected config just runs against it. An app with several — a flagship monorepo with a Postgres orders database and a separate analytics database — prompts a select menu in an interactive terminal:

heximon migrations migrate
? Which database? ›
❯ All databases
  orders.config.ts
  analytics.config.ts

In a non-interactive shell (CI) that same run defaults to all detected configs rather than hanging on a prompt with no TTY to answer it. Pass --config <path> to target one file directly, bypassing detection entirely — the command still loads and duck-types it, so an unusual location or filename works too.

Commands

Every subcommand takes --cwd (project root), --env (the .env.<mode> overlay to load — default development), --env-file (an explicit dotenv file layered over the cascade), and --config (target one file, skip detection).

SubcommandRunsNotes
generatedrizzle-kit generateDiffs your schema, writes a new migration folder. --name labels it.
migratedrizzle-kit migrateApplies pending migrations. Skips (with a message) any config whose driver applies outside drizzle-kit.
statusLists the .sql files already on disk per database — no live query, so it can't tell applied from pending.
pushdrizzle-kit pushPushes the schema straight to the database, no migration files. Destructive — dev only.

push is the one command that touches your schema without a reviewable diff, so it refuses to run unattended: pass --yes to confirm non-interactively, or answer the confirmation prompt in a terminal.

terminal
pnpx heximon migrations generate --name add-users-index
pnpx heximon migrations push --yes   # dev database only
status can't tell you what's applied. It lists the migration files under your out folder, not what the live database has run — that needs a query against the database's own bookkeeping table, which this view doesn't make. For that, inject the MigrationRunner and call status() from your app instead.

.env is loaded for you

Before detecting anything, the command loads the dotenv cascade — .env < .env.local < .env.<mode> < .env.<mode>.local, shell wins — into process.env, so the spawned drizzle-kit child inherits DATABASE_URL (or whatever your config's getter reads) without you exporting it by hand. An explicit --env-file layers on top of the cascade rather than replacing it, for a one-off like migrating against a .env.production snapshot locally.

terminal
pnpx heximon migrations migrate --env production
pnpx heximon migrations migrate --env-file .env.production

The same detector drives deploy-time migrations

heximon deploy --migrate (see Deploying a Heximon app) reuses this exact detection — every drizzle-kit-mode config becomes a target the deploy applies in one pass, sequenced around the ship.

The one deliberate difference: a deploy never loads your dev .env cascade (a stray dev DATABASE_URL migrating the wrong database during a production ship would be a bad day), inheriting only the deploy's own environment plus --env-file.

See also

  • Database migrations — the unified config, the injectable MigrationRunner, and applying migrations on boot.
  • Deploying a Heximon app — the pre-ship migration gate and --migrate apply step.
  • Drizzle ORM — the per-dialect config classes (DrizzleLibSQLConfig, DrizzlePgConfig, DrizzleMySQL2Config) this command detects.
Copyright © 2026