Database migrations
without the pain.
GoMigrate is a friendly wrapper around golang-migrate with short commands, YAML config, and an interactive terminal UI. Works as a CLI and as a Go library.
00 Installation
GoMigrate has three audiences, each with a different installation path. Most developers fall into one of the first two — you only need to clone the repo if you want to contribute code.
gomigrate command in your terminal.Path A — Install the CLI
With Go 1.22+ installed, a single command:
$ go install github.com/taqnihub/gomigrate@latest
Verify the installation:
$ gomigrate --version
gomigrate version 0.1.0
Make sure $(go env GOPATH)/bin is in your $PATH.
If gomigrate isn't found after install, add this to your shell config:
$ export PATH="$PATH:$(go env GOPATH)/bin"
Path B — Use as a Go library
Inside your Go project, add it as a dependency:
$ go get github.com/taqnihub/gomigrate@latest
Then import the packages you need:
import (
"github.com/taqnihub/gomigrate/config"
"github.com/taqnihub/gomigrate/migrate"
)
See Part Two below for usage examples.
config loads and validates configuration.
migrate contains the engine and operations (Up, Down, Version, etc.).
Import either or both.
Path C — Clone the repo (contributors only)
This path is only for developers who want to contribute code, fix bugs, or modify the library itself. You don't need this to use gomigrate.
$ git clone https://github.com/taqnihub/gomigrate.git
$ cd gomigrate
$ go build -o gomigrate .
$ ./gomigrate --help
See CONTRIBUTING.md for the development workflow,
code style expectations, and how to run tests.
01 Quick Start
Copy-paste this into your terminal and you'll be running migrations in under a minute. Every step is intentional — skip none.
# 1. Install the CLI
$ go install github.com/taqnihub/gomigrate@latest
# 2. Verify it's installed
$ gomigrate --version
# 3. Initialize gomigrate in your project (interactive wizard)
$ gomigrate init
# 4. Create your first migration (generates .up.sql and .down.sql)
$ gomigrate create add_users_table
# 5. Edit the generated SQL files in ./db/migrations/
# (add your CREATE TABLE, ALTER TABLE, etc.)
# 6. Check what's pending
$ gomigrate status
# 7. Apply all pending migrations
$ gomigrate up
# 8. Verify everything was applied
$ gomigrate status
# 9. If you need to undo the last migration
$ gomigrate down
# 10. Or launch the interactive TUI menu
$ gomigrate
No Docker commands. No 200-character invocations. You're already more productive
than someone using raw golang-migrate.
PART ONE Using the CLI
The fastest way to use GoMigrate is as a standalone command-line tool. Install it once, drop a config file in your project, and run short, memorable commands instead of 200-character Docker invocations.
Complete command reference
Every command gomigrate understands, grouped by purpose. Each row shows the command syntax, a description, and a concrete example you can run.
| Command | Description | Example |
|---|---|---|
| Setup | ||
| gomigrate --version | Show gomigrate version and build info | gomigrate --version |
| gomigrate --help | Show help for all commands | gomigrate --help |
| gomigrate <cmd> --help | Show help for a specific subcommand | gomigrate up --help |
| gomigrate init | Interactive wizard to create .gomigrate.yml in the current directory |
gomigrate init |
| gomigrate init --force | Overwrite existing config without prompting | gomigrate init --force |
| Migrations | ||
| gomigrate create <name> | Generate a pair of timestamped up/down SQL files. Refuses duplicate names. | gomigrate create add_users_table |
| gomigrate up | Apply all pending migrations in order | gomigrate up |
| gomigrate up [n] | Apply only the next N pending migrations | gomigrate up 3 |
| gomigrate down | Revert the most recently applied migration (safe default) | gomigrate down |
| gomigrate down [n] | Revert the last N applied migrations | gomigrate down 2 |
| gomigrate down --all | Revert ALL applied migrations — destructive, use with care | gomigrate down --all |
| Inspection | ||
| gomigrate status | Show all migrations with applied vs pending status | gomigrate status |
| Recovery | ||
| gomigrate force <version> | Force-set version without running SQL. Fixes dirty state after a failed migration. | gomigrate force 20260417103045 |
| Interactive Mode | ||
| gomigrate | Launch the TUI menu when called with no arguments. Navigate with arrow keys. | gomigrate |
Global flags (work with any command)
| Flag | Short | Description | Example |
|---|---|---|---|
| --config <path> | -c | Use a specific config file instead of searching | gomigrate -c prod.yml up |
| --driver <name> | -d | Override database driver (mysql or postgres) |
gomigrate -d postgres status |
| --dir <path> | Override migrations directory | gomigrate --dir ./db/migs up | |
| --verbose | -v | Show detailed output for debugging | gomigrate -v up |
| --no-interactive | Disable TUI and colors — ideal for CI/CD pipelines | gomigrate --no-interactive up |
Initialize a project
Inside the root of your project, run gomigrate init. You'll be walked through an
interactive wizard that asks for your driver, connection details, and migrations directory.
$ gomigrate init
🚀 Welcome to GoMigrate
? Database driver:
> MySQL
PostgreSQL
? Host: localhost
? Port: 3306
? Database name: myapp
? Username: root
? Password: ********
? Migrations directory: ./db/migrations
✓ GoMigrate initialized!
Config: /path/to/project/.gomigrate.yml
Migrations: /path/to/project/db/migrations
This creates two things:
.gomigrate.yml— a config file with your connection details./db/migrations/— an empty directory where your SQL files will live
Creating a migration
Run gomigrate create with a descriptive name. Two timestamped SQL files will be
generated — one for applying the change, one for reverting it.
$ gomigrate create add_users_table
✨ Creating Migration
✓ Created migration files
UP: db/migrations/20260417103045_add_users_table.up.sql
DOWN: db/migrations/20260417103045_add_users_table.down.sql
Next: edit the files with your SQL, then run gomigrate up
Naming conventions
| Pattern | Example |
|---|---|
create_<table>_table | create_users_table |
add_<column>_to_<table> | add_email_to_users |
drop_<column>_from_<table> | drop_legacy_id_from_orders |
add_index_<name> | add_index_email_to_users |
Now open the generated files and write your SQL. Example for users:
-- up.sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- down.sql
DROP TABLE IF EXISTS users;
GoMigrate refuses to create two migrations with the same name and tells you which existing files conflict. This prevents silent duplicates that crash your migrations at runtime.
Applying & reverting
To apply every pending migration in order, just run:
$ gomigrate up
⚡ Applying Migrations
Database: mysql on localhost:3306/myapp
Will apply 3 migration(s):
→ 20260417103045 add_users_table
→ 20260417103112 add_products_table
→ 20260417103158 add_orders_table
✓ 20260417103045 add_users_table
✓ 20260417103112 add_products_table
✓ 20260417103158 add_orders_table
✓ Applied 3 migration(s) in 12ms
Version: 0 → 20260417103158
Apply only the next N migrations by passing a number:
$ gomigrate up 1 # apply just the next one
$ gomigrate up 3 # apply the next three
Reverting
To undo the most recent migration:
$ gomigrate down
⬇ Reverting Migrations
Database: mysql on localhost:3306/myapp
Will revert 1 migration(s):
← 20260417103158 add_orders_table
✓ 20260417103158 add_orders_table
✓ Reverted 1 migration(s) in 6ms
Version: 20260417103158 → 20260417103112
Revert N migrations, or use --all to revert everything:
$ gomigrate down 3 # revert the last three
$ gomigrate down --all # revert ALL (destructive)
down --all drops every table managed by GoMigrate. Always double-check the
connected database before running it in production.
Checking status
See which migrations are applied and which are pending:
$ gomigrate status
📋 Migration Status
Database: mysql on localhost:3306/myapp
Directory: ./db/migrations
VERSION NAME STATUS
──────────────────────────────────────────────────────
20260417103045 add_users_table ✓ applied
20260417103112 add_products_table ✓ applied
20260417103158 add_orders_table ○ pending
3 total · 2 applied · 1 pending
Fixing a dirty state
If a migration fails halfway through, GoMigrate marks the database as dirty and refuses to run further migrations until you resolve it. First, manually fix the schema in your database. Then tell GoMigrate what version the schema actually matches:
$ gomigrate force 20260417103112
🔧 Forcing Migration Version
✓ Version forced to 20260417103112
⚠ No SQL was executed.
Make sure your database schema matches this version!
force doesn't run any SQL. It only updates the schema_migrations table.
You must ensure your actual schema matches the version you claim to be at.
Interactive mode
Run gomigrate with no arguments to get a keyboard-navigable menu:
$ gomigrate
⚡ GoMigrate
mysql · localhost:3306 · myapp
What would you like to do?
> ▶ Apply migrations
◀ Revert last migration
✚ Create new migration
📋 View status
🔧 Force version
q Quit
↑/↓ navigate · enter select · q quit
Use arrow keys (or j/k for vim users) to move, Enter
to select, and q or Esc to quit.
Configuration
GoMigrate reads settings from multiple sources, applied in this order (highest priority first):
- CLI flags — e.g.
--driver postgres - Environment variables with the
GOMIGRATE_prefix .gomigrate.ymlin the current directory.gomigrate.ymlin your home directory- Sensible defaults
Config file reference
# .gomigrate.yml
driver: mysql # mysql or postgres
host: localhost
port: 3306
database: myapp
user: root
password: secret
migrations_dir: ./db/migrations
# Optional
ssl_mode: disable # disable | require | verify-full
timezone: UTC
lock_timeout: 15s
Using environment variables
Any config value can be overridden with a GOMIGRATE_-prefixed variable. This is how you should handle secrets in CI/CD:
$ export GOMIGRATE_DRIVER=postgres
$ export GOMIGRATE_HOST=prod-db.example.com
$ export GOMIGRATE_PASSWORD="$DB_PASSWORD"
$ gomigrate up
PART TWO As a Go library
Everything the CLI does is also available as a Go library. Import config and migrate
packages directly into your application to run migrations programmatically — perfect for
auto-migrating on app startup, embedding migrations in your deployment pipeline, or building
your own migration tooling.
You already installed the library in the Installation section above —
if you skipped that, just run go get github.com/taqnihub/gomigrate@latest inside
your Go project.
| Package | Purpose |
|---|---|
github.com/taqnihub/gomigrate/config |
Load and validate configuration from files, env vars, or programmatically. |
github.com/taqnihub/gomigrate/migrate |
Run migrations — Up, Down, Version, Create, Force, List. |
Basic usage
The canonical five-line example:
package main
import (
"log"
"github.com/taqnihub/gomigrate/config"
"github.com/taqnihub/gomigrate/migrate"
)
func main() {
cfg, _ := config.Load("") // reads .gomigrate.yml + env vars
engine, _ := migrate.New(cfg)
defer engine.Close()
if err := engine.Up(); err != nil {
log.Fatal(err)
}
}
This does exactly what gomigrate up does on the command line — but from inside your own Go program.
Auto-migrate on app startup
A common pattern: have your web server apply any pending migrations before it starts serving traffic. This guarantees your schema is always in sync with your deployed code.
package main
import (
"errors"
"log"
"net/http"
"github.com/taqnihub/gomigrate/config"
"github.com/taqnihub/gomigrate/migrate"
)
func main() {
// 1. Load config
cfg, err := config.Load("")
if err != nil {
log.Fatalf("config: %v", err)
}
// 2. Create engine and apply pending migrations
engine, err := migrate.New(cfg)
if err != nil {
log.Fatalf("migrate init: %v", err)
}
defer engine.Close()
if err := engine.Up(); err != nil {
// ErrNoChange means we're already up-to-date — that's fine
if !errors.Is(err, migrate.ErrNoChange) {
log.Fatalf("migration failed: %v", err)
}
}
// 3. Log current state
version, dirty, _ := engine.Version()
log.Printf("DB at version %d (dirty=%v)", version, dirty)
// 4. Start your server
log.Fatal(http.ListenAndServe(":8080", nil))
}
Use errors.Is(err, migrate.ErrNoChange) to ignore the "nothing to apply" case.
Every other error should halt startup — you don't want to serve traffic with an outdated schema.
Programmatic configuration
You don't need a YAML file. Build a config.Config struct directly — useful
when your app composes config from multiple sources (secret managers, Consul, AWS Parameter Store, etc.):
package main
import (
"log"
"os"
"github.com/taqnihub/gomigrate/config"
"github.com/taqnihub/gomigrate/migrate"
)
func main() {
cfg := &config.Config{
Driver: "postgres",
Host: os.Getenv("DB_HOST"),
Port: 5432,
Database: os.Getenv("DB_NAME"),
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
MigrationsDir: "./db/migrations",
SSLMode: "require",
}
if err := cfg.Validate(); err != nil {
log.Fatalf("invalid config: %v", err)
}
engine, err := migrate.New(cfg)
if err != nil {
log.Fatal(err)
}
defer engine.Close()
if err := engine.Up(); err != nil {
log.Fatal(err)
}
}
API reference
Package: config
| Function / Method | Description |
|---|---|
config.Load(path string) (*Config, error) |
Loads config from the given path, env vars, and defaults. Pass "" to use default search paths. |
config.Default() *Config |
Returns a *Config populated with sensible defaults. |
(*Config).Validate() error |
Returns an error if required fields are missing or invalid. |
(*Config).DSN() (string, error) |
Builds the driver-specific connection string. |
Package: migrate
| Function / Method | Description |
|---|---|
migrate.New(cfg *config.Config) (*Engine, error) |
Creates a new migration engine. The migrations directory is auto-created if missing. |
(*Engine).Close() error |
Releases database and source resources. Call via defer. |
(*Engine).Up() error |
Apply all pending migrations. Returns ErrNoChange if already up-to-date. |
(*Engine).UpN(n int) error |
Apply the next n pending migrations. |
(*Engine).Down(n int) error |
Revert the last n applied migrations. |
(*Engine).DownAll() error |
Revert every applied migration. Destructive — use with caution. |
(*Engine).Version() (uint, bool, error) |
Returns current version, whether dirty, and any error. |
(*Engine).Force(version int) error |
Force-set the version without running SQL. Use to recover from dirty state. |
(*Engine).Create(name string) (up, down string, err error) |
Create a new pair of up/down SQL files. Returns the paths. |
(*Engine).List() ([]MigrationFile, error) |
List all migration files on disk, sorted ascending by version. |
Sentinel errors
| Error | Meaning |
|---|---|
migrate.ErrNoChange |
Returned by Up, Down, etc. when there's nothing to do.
Check with errors.Is(err, migrate.ErrNoChange).
|
MIT © taqnihub · Built with ⚡ golang-migrate, cobra, viper & bubbletea.