Database Commands

Database migration tools including SQL and Go migrations, app-scoped migrations, and migration management

Database Commands

The forge db command provides comprehensive database migration tools. It supports SQL and Go migrations, app-scoped migration isolation, multiple database connections, and safe rollback operations. Migrations are powered by bun/migrate under the hood.

Main Command

forge db (alias: database)

Database management tools.

forge db migrate
forge db status
forge db rollback
forge database migrate    # Full alias

Note: The main command requires a subcommand. Use forge db --help to see all available subcommands.

Common Flags

Most forge db subcommands accept these flags for database connection and scoping:

FlagAliasDefaultDescription
--database-d"default"Database name from project config
--dsn""Override database DSN/connection string
--type-t""Override database type (postgres, mysql, sqlite, mongodb)
--app-a""App name for app-scoped migrations
--verbose-vfalseShow debug output (paths, table names, etc.)

Subcommands

forge db init

Initialize migration tables in the database and create the migrations directory.

forge db init
forge db init --database=analytics
forge db init --app=api-gateway
forge db init --dsn="postgres://user:pass@localhost/mydb"

Behavior:

  • Creates the migration tracking tables (bun_migrations and bun_migration_locks)
  • Ensures the migrations directory exists
  • Generates a migrations.go bootstrap file if missing
  • When --app is provided, creates app-scoped tracking tables (e.g., bun_migrations_api_gateway)

forge db migrate

Run all pending migrations.

forge db migrate                          # Run all pending migrations
forge db migrate --database=analytics     # Use a named database connection
forge db migrate --app=api-gateway        # Run app-scoped migrations only
forge db migrate --dsn="postgres://..."   # Override connection string
forge db migrate --verbose                # Show debug information

Behavior:

  • Discovers and runs all pending migrations in order
  • Auto-initializes migration tables if they do not exist
  • Detects Go migrations automatically and builds a temporary runner binary
  • When --app is provided, uses app-scoped migration directory and tracking tables
  • Shows a spinner with progress information

If both SQL and Go migration files are present, Forge builds a temporary Go binary that includes all migrations (SQL and Go) and runs them together.


forge db rollback

Rollback the last group of applied migrations.

forge db rollback                      # Rollback last migration group
forge db rollback --app=api-gateway    # Rollback app-scoped migrations
forge db rollback --database=analytics # Use a named database connection

Behavior:

  • Prompts for confirmation before rolling back
  • Rolls back the last migration group (all migrations applied in the same batch)
  • Supports both SQL and Go migrations
  • When --app is provided, rolls back only that app's migrations

forge db status

Show migration status -- which migrations have been applied and which are pending.

forge db status
forge db status --app=api-gateway
forge db status --database=analytics

Behavior:

  • Lists all applied migrations with group ID and applied timestamp
  • Lists all pending migrations
  • Shows a helpful message when all migrations are up to date

Example Output:

Applied migrations:
  Name                          Group   Applied At
  20240115100000_create_users     1     2024-01-15 10:00:05
  20240115100100_create_products  1     2024-01-15 10:00:05
  20240120090000_add_user_email   2     2024-01-20 09:00:03

Pending migrations:
  20240125140000_create_orders
  20240130110000_add_indexes

forge db reset

Reset the database by rolling back all migrations and re-running them.

forge db reset                   # Interactive confirmation
forge db reset --force           # Skip confirmation
forge db reset --app=api-gateway # Reset app-scoped migrations only

Flags:

  • --force / -f -- Skip the confirmation prompt

This is a destructive operation. It rolls back all applied migrations and then re-runs them from scratch. Always back up your database before running this in production.


forge db create-sql

Create a new pair of SQL migration files (up and down).

forge db create-sql add_users_table
forge db create-sql add_orders_table --tx
forge db create-sql add_user_roles --app=auth-service

Flags:

  • --tx -- Create transactional migration files (.tx.up.sql / .tx.down.sql)
  • --app / -a -- Create migration in an app-scoped directory

Behavior:

  • Generates timestamped migration files in the appropriate directory
  • Without --tx: creates {timestamp}_{name}.up.sql and {timestamp}_{name}.down.sql
  • With --tx: creates {timestamp}_{name}.tx.up.sql and {timestamp}_{name}.tx.down.sql

Example:

forge db create-sql create_users_table
# Created: database/migrations/20240115100000_create_users_table.up.sql
# Created: database/migrations/20240115100000_create_users_table.down.sql

The generated files are empty templates for you to fill in:

-- database/migrations/20240115100000_create_users_table.up.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- database/migrations/20240115100000_create_users_table.down.sql
DROP TABLE IF EXISTS users;

forge db create-go

Create a new Go migration file.

forge db create-go add_seed_data
forge db create-go populate_defaults --app=api-gateway

Flags:

  • --app / -a -- Create migration in an app-scoped directory

Behavior:

  • Creates a single .go migration file with init() function boilerplate
  • Go migrations can execute arbitrary Go code during migration (useful for data transformations, seeding, etc.)

Go migrations require a migrations.go file in the migrations directory. Run forge db init to generate it if missing.


forge db lock

Manually acquire the migration lock. Prevents other processes from running migrations concurrently.

forge db lock
forge db lock --app=api-gateway

forge db unlock

Manually release the migration lock.

forge db unlock
forge db unlock --app=api-gateway

You typically do not need to use lock/unlock manually. Forge acquires and releases the lock automatically when running migrate, rollback, or reset. Use these commands only to recover from a stuck lock (e.g., after a crash).


forge db mark-applied

Mark all pending migrations as applied without actually running them.

forge db mark-applied
forge db mark-applied --app=api-gateway

Behavior:

  • Prompts for confirmation
  • Marks all pending migrations as applied in the tracking table using no-op execution
  • Useful when migrations were applied manually or outside of Forge

App-Scoped Migrations

App-scoped migrations allow you to isolate migrations for individual applications within a monorepo. Each app gets its own migration directory and its own tracking tables in the database.

How It Works

When you pass the --app flag, Forge:

  1. Resolves a separate migration directory for that app
  2. Uses namespaced tracking tables so migrations don't interfere with other apps
# Create a migration for the api-gateway app
forge db create-sql create_routes --app=api-gateway

# Run only api-gateway migrations
forge db migrate --app=api-gateway

# Check status for api-gateway only
forge db status --app=api-gateway

Migration Directory Resolution

Forge resolves app-scoped migration paths using a two-step process:

  1. App config override: If the app has its own .forge.yaml with database.migrations_path, that path is used (relative to the app directory).
  2. Centralized convention: Otherwise, Forge creates a subdirectory under the global migrations path: <global_migrations_path>/<app-name>/.
# Default centralized layout
database/
  migrations/
    20240115_create_shared_table.up.sql      # Global migrations
    20240115_create_shared_table.down.sql
    api-gateway/                              # App-scoped
      20240120_create_routes.up.sql
      20240120_create_routes.down.sql
    auth-service/                             # App-scoped
      20240120_create_sessions.up.sql
      20240120_create_sessions.down.sql

App Config Override

To use a custom migration path for an app, add a database section to the app's .forge.yaml:

# apps/api-gateway/.forge.yaml
app:
  name: api-gateway

database:
  migrations_path: db/migrations   # Relative to this app's directory
  seeds_path: db/seeds

This results in migrations stored at apps/api-gateway/db/migrations/ instead of the centralized location.

Table Namespacing

App-scoped migrations use namespaced tracking tables to prevent conflicts:

App NameMigration TableLock Table
(global)bun_migrationsbun_migration_locks
api-gatewaybun_migrations_api_gatewaybun_migration_locks_api_gateway
auth-servicebun_migrations_auth_servicebun_migration_locks_auth_service

App names are sanitized for use as SQL identifiers: lowercased, with hyphens, dots, and spaces replaced by underscores.


Go Migrations

Go migrations allow you to write migrations in Go for complex data transformations, seeding, or operations that are difficult to express in pure SQL.

Setup

  1. Initialize the migrations directory:
forge db init

This creates a migrations.go file that registers the migration package.

  1. Create a Go migration:
forge db create-go seed_default_roles
  1. Edit the generated file:
package migrations

import (
    "context"
    "fmt"

    "github.com/uptrace/bun"
)

func init() {
    Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
        // Up migration
        fmt.Println("Seeding default roles...")
        _, err := db.NewInsert().
            Model(&Role{Name: "admin"}).
            Exec(ctx)
        return err
    }, func(ctx context.Context, db *bun.DB) error {
        // Down migration
        _, err := db.NewDelete().
            Model((*Role)(nil)).
            Where("name = ?", "admin").
            Exec(ctx)
        return err
    })
}

How Go Migrations Run

When Forge detects .go files in the migrations directory (besides migrations.go), it:

  1. Generates a temporary migration runner binary
  2. The runner imports your migration package and connects to the database
  3. Executes the requested command (migrate, rollback, status, etc.)
  4. Cleans up the temporary binary

This approach means you can mix SQL and Go migrations in the same directory. They are applied in timestamp order regardless of type.

Mixing SQL and Go Migrations

You can freely mix SQL and Go migration files:

database/migrations/
  migrations.go                              # Package registration
  20240115100000_create_users.up.sql         # SQL migration
  20240115100000_create_users.down.sql
  20240120090000_seed_default_data.go        # Go migration
  20240125140000_add_indexes.tx.up.sql       # Transactional SQL migration
  20240125140000_add_indexes.tx.down.sql

Database Connection

Connecting via Config

Define database connections in your project's .forge.yaml or config.yaml:

# .forge.yaml
database:
  connections:
    default:
      dsn: "postgres://user:pass@localhost:5432/myapp?sslmode=disable"
    analytics:
      dsn: "postgres://user:pass@localhost:5432/analytics?sslmode=disable"

Use the --database flag to select a named connection:

forge db migrate --database=analytics

Connecting via DSN

Override the connection string directly:

forge db migrate --dsn="postgres://user:pass@localhost:5432/mydb?sslmode=disable"

Environment Variables

Database connections support environment variable expansion. Forge loads .env files in this priority order:

  1. .env -- Base environment
  2. .env.local -- Local overrides (gitignored)
  3. .env.{environment} -- Environment-specific (e.g., .env.production)
  4. .env.{environment}.local -- Environment-specific local overrides

The environment is determined by FORGE_ENV or GO_ENV (defaults to development).

# .env
DATABASE_URL=postgres://user:pass@localhost:5432/myapp?sslmode=disable
# .forge.yaml (references the env variable)
database:
  connections:
    default:
      dsn: "${DATABASE_URL}"

Migration File Conventions

SQL Migrations

SQL migration files use this naming pattern:

PatternDescription
{timestamp}_{name}.up.sqlStandard up migration
{timestamp}_{name}.down.sqlStandard down migration
{timestamp}_{name}.tx.up.sqlTransactional up migration
{timestamp}_{name}.tx.down.sqlTransactional down migration

Transactional migrations (created with --tx) wrap the entire migration in a database transaction, ensuring atomicity.

Go Migrations

Go migration files use the pattern {timestamp}_{name}.go and must be in the same package as migrations.go.


Command Reference

CommandDescriptionDatabase FlagsApp-ScopedGo Support
forge db initInitialize migration tablesYesYes--
forge db migrateRun pending migrationsYesYesYes
forge db rollbackRollback last migration groupYesYesYes
forge db statusShow migration statusYesYesYes
forge db resetReset database (rollback all + rerun)YesYes--
forge db create-sqlCreate SQL migration files--Yes--
forge db create-goCreate Go migration file--Yes--
forge db lockAcquire migration lockYesYes--
forge db unlockRelease migration lockYesYes--
forge db mark-appliedMark pending as appliedYesYes--

"Database Flags" means the command accepts --database, --dsn, and --type.


Best Practices

  1. Version control migrations -- Always commit migration files. They are the source of truth for your database schema.
  2. Write reversible migrations -- Always include a down migration for safe rollback.
  3. Keep migrations small and focused -- One logical change per migration is easier to review and rollback.
  4. Use transactional migrations -- Use --tx for DDL changes that should be atomic.
  5. Use Go migrations for data transformations -- Complex data seeding or transformation logic is better expressed in Go than SQL.
  6. Use app-scoped migrations in monorepos -- Isolate each app's schema changes to avoid conflicts and enable independent deployments.
  7. Back up before production migrations -- Always have a backup before running migrations in production.
  8. Test in development first -- Run forge db migrate locally, verify with forge db status, then promote to staging and production.

Troubleshooting

No Migrations Found

forge db migrate
# No migrations found

Solution: Create your first migration:

forge db create-sql initial_schema
# or for app-scoped:
forge db create-sql initial_schema --app=my-app

Database Connection Error

forge db migrate
# Error: database connection failed

Solution:

# Check your database is running
pg_isready -h localhost -p 5432

# Verify your DSN
forge db migrate --dsn="postgres://user:pass@localhost:5432/mydb" --verbose

# Check your .forge.yaml or config.yaml
cat .forge.yaml

Migration Lock Stuck

If a previous migration run crashed and left the lock acquired:

forge db unlock
# or for app-scoped:
forge db unlock --app=api-gateway

Missing migrations.go

If you see a warning about a missing migrations.go file:

forge db init
# or for app-scoped:
forge db init --app=api-gateway

This generates the required package registration file for Go migrations.

How is this guide?

On this page