Bunny Database migrations from the terminal with the bunny.net CLI

Posted by:

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.