Database migrations start simple. You need a users table, so you write some SQL. A week later you add an index, someone else adds a column, production needs another change, and staging is one migration ahead. Before long the SQL lives in a mix of folders, Slack messages, deployment scripts, and someone's Downloads directory.
Writing the SQL is the easy part. The part that gets messy is keeping track of what ran, where, and in what order.
bunny db now has a migration workflow built in. It's there for when you're using Bunny Database for the first time, starting a new project, or still haven'tsettled on a way to manage schema changes. Keep the migration workflow you already have if you like it.
Your first migration
Say we're building a small application backed by Bunny Database. We need somewhere to store users, so we create the first migration:
bunny db migrations create create_users
The CLI creates an empty, numbered SQL file:
migrations/ └── 0001_create_users.sql
Leave the name out and the CLI will ask you for one:
bunny db migrations create
Then open the file and write the schema change:
CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP );
The filename matters as much as the SQL inside it. It identifies the migration, and the numeric prefix decides the order things run in. As the application grows, the directory becomes a record of how the schema got to where it is:
migrations/ ├── 0001_create_users.sql ├── 0002_add_display_name.sql └── 0003_add_email_index.sql
Those files live alongside the code that depends on them, so they get committed to your repository and shared with everyone else on the team. The order is written into the filenames, and nothing important ends up sitting in a Downloads folder.
See what's waiting
Before you change anything, you can check where things stand:
bunny db migrations list
The CLI compares the migration files in your project against the migrations already applied to the database, then shows which are applied and which are still pending.
That's especially useful when others are involved. A teammate adds a migration for avatars, commits it with the application code that needs it, and pushes. You pull their branch and now have:
migrations/ ├── 0001_create_users.sql ├── 0002_add_display_name.sql ├── 0003_add_email_index.sql └── 0004_add_avatar_url.sql
Run list and you'll see that the first three applied and the fourth still waiting, so nobody has to ask whether they already ran this one.
You can also preview the run without touching anything:
bunny db migrations apply --dry-run
A dry run really is dry: it won't create the migration tracking table or change your database.
If it all looks right, apply the pending migrations:
bunny db migrations apply
The CLI runs them in order and records each successful migration in the database, so running apply again won't replay anything that has already gone through.
Each migration is applied together with the record that tracks it, as a single atomic operation. Either the migration succeeds and gets recorded, or neither happens. If one fails, the CLI stops there and leaves the remaining files alone. Fix the problem and run apply again; the failed migration is still pending.
The upshot is that a migration travels with the code that needs it. Nobody has to maintain a shared document reminding the team to run add-avatar-final-v2.sql, and nobody has to guess whether the SQL file in that folder ever made it to production.
Catching migration drift
There's a version of the scattered-migration problem that's harder to spot: someone changes an old migration after it has already run. Maybe 0002_add_display_name.sql gets edited during a refactor, or an old migration disappears from the repository entirely.
The CLI stores a checksum when it applies a migration, so bunny db migrations list can tell when the files on disk no longer match what the database remembers. Along with applied and pending migrations, it flags any that have been modified or are missing.
It won't guess what you meant, and it won't rewrite your database history to make the mismatch go away. The fix is usually the same one: leave the applied migration alone and write a new migration on top of it.
bunny db migrations create fix_display_name
Already using a migration tool
The best migration tool is often the one your project already has, and the built-in workflow isn't a requirement for using Bunny Database. If your application manages its schema with Drizzle, a Go migration tool, or your framework's own migration system, point it at your Database and carry on as usual. Your migrations can stay exactly where they are.
bunny db migrations is there for projects starting from an empty schema.
Generate with Drizzle, apply with the CLI
There's a middle ground for ORMs that generate plain SQL. Drizzle owns the schema and writes each migration file, and the bunny.net CLI applies that SQL to your Database and records what has run.
drizzle-kit generate bunny db migrations apply
When there's no migrations/ folder, the CLI picks up drizzle/ on its own, splits each file at Drizzle's statement breakpoints, and leaves the meta/ journal alone. If you keep both folders, name the one you mean:
bunny db migrations apply --dir drizzle
The appeal is what you don't have to set up. The CLI finds the database from your .env and signs in with your bunny.net API key, so your app carries no database URL or long-lived token for migrations and you write no migrate script. You also get --dry-run, bunny db migrations list, and drift detection over the generated files.
Two rules keep this tidy. Generate every migration with Drizzle so it lands in Drizzle's journal, and skip bunny db migrations create in this setup. Pick one applier.
A default for your first database
If this is your first Bunny Database, you probably don't want to pick a migration framework before you've created your first table. The built-in workflow gives you something to use in the meantime, with no extra tool to install and no framework-specific schema format to learn. You write plain SQL, keep it with your application, and let the CLI handle ordering and tracking.
Four commands cover it:
# Create a migration bunny db migrations create add_users # Check migration state bunny db migrations list # Preview what will run bunny db migrations apply --dry-run # Apply pending migrations bunny db migrations apply
If your application eventually grows into a framework with its own migration system, use that one. When you create your first Bunny Database and wonder how you're meant to manage schema changes, there's an answer already sitting in the CLI.
Local development and CI
The same workflow works when nobody is sitting at the terminal. Interactively, apply asks for confirmation before it changes the database. In a non-interactive environment such as CI it skips the prompt, so your deployment isn't stuck waiting for someone to answer it. You can skip confirmation explicitly too:
bunny db migrations apply --force
Once the migration files are committed alongside your application, your deployment pipeline can apply whatever migrations came with a release:
bunny db migrations apply
The same files you used in development travel through the pipeline with the code that depends on them, so there's no separate pile of production SQL scripts to maintain.
Get started
If you already have a migration tool you love, keep using it. If you don't, getting started takes a few commands:
npm install -g @bunny.net/cli bunny login bunny db migrations create my_first_migration
Write some SQL, check what's waiting, and apply it:
bunny db migrations list bunny db migrations apply
Your schema changes stay next to the code that depends on them, your database remembers what it has already applied, and that folder of mystery SQL files turns into something you can read as a history.
The bunny.net CLI is open source and built in public. If something's broken, or you've got an idea for making migrations better, come talk to us on GitHub or Discord.
Comments require cookies. to view and post.

