Skip to main content
A migration is a class implementing IMigration, registered with @decorator.migration(). The decorator adds the class to the container and to the migrations registry, so the runner can resolve and order it. up() applies the change and down() reverses it.

Scaffold a migration

Generate a timestamped migration class with the CLI. talos migration:create writes a migration class under modules/<module>/src/migrations/, a matching test under modules/<module>/tests/migrations/, refreshes the migrations.ts barrel export, and creates the module’s bin/migration/up.ts and bin/migration/down.ts runners if they are missing.
# Generate a migration in the shared module
talos migration:create

# Target a specific module
talos migration:create --module=auth
OptionDescriptionDefault
--moduleTarget module the migration is generated into.shared
The migration file name is derived from a timestamp version automatically, so there is no --name option. The generated class is a ready-to-fill stub:
import { decorator, type IMigration, type MigrationClassType } from "@talosjs/migrations";
import type { TransactionSQL } from "bun";

@decorator.migration()
export class Migration20240115103045 implements IMigration {
  public async up(tx: TransactionSQL): Promise<void> {
    // await tx`...`;
  }

  public async down(tx: TransactionSQL): Promise<void> {
    // await tx`...`;
  }

  public getVersion(): string {
    return "20240115103045";
  }

  public getDependencies(): MigrationClassType[] {
    return [];
  }
}
See the migration:create command for the full reference.

Implement up() and down()

Fill in up() with the schema change and down() with its exact reverse. Both receive the transaction (tx) the runner wraps around the migration and the pooled sql client:
import { decorator } from "@talosjs/migrations";
import type { IMigration, MigrationClassType } from "@talosjs/migrations";
import type { SQL, TransactionSQL } from "bun";

@decorator.migration()
export class Migration20240115103045 implements IMigration {
  public async up(tx: TransactionSQL, sql: SQL): Promise<void> {
    await tx`CREATE TABLE users (
      id SERIAL PRIMARY KEY,
      email VARCHAR(255) NOT NULL UNIQUE,
      created_at TIMESTAMP DEFAULT NOW()
    )`;
  }

  public async down(tx: TransactionSQL, sql: SQL): Promise<void> {
    await tx`DROP TABLE IF EXISTS users`;
  }

  public getVersion(): string {
    return "20240115103045";
  }

  public getDependencies(): MigrationClassType[] {
    return [];
  }
}

Order with dependencies

The runner sorts migrations by version, but a migration’s dependencies always run first regardless of timestamp. Declare them in getDependencies() when one change must precede another:
public getDependencies(): MigrationClassType[] {
  return [Migration20240115103045];
}
Before running a migration, the runner resolves each dependency from the container and runs it first, recursively.

Migrations that hold up

Make down() the exact reverse of up(): drop precisely what up adds, since an asymmetric or irreversible down() is a bug. Once a migration has run on a shared database, never edit it — add a new corrective migration instead of rewriting one that already ran. Add indexes for foreign keys, WHERE and ORDER BY columns, and unique constraints in up(), then drop them in down() before the table or column they cover. Keep column types, nullability, and lengths in sync with the entity definition, so a non-nullable column is also NOT NULL in the migration.

Next steps