Telaio
CLI

Migrations

Create, run, roll back, and inspect database migrations using the telaio migrate subcommands.

Migrations

Telaio wraps Kysely's migration system with a set of CLI subcommands. Migrations are TypeScript files with up and down exports, discovered from a directory on disk.

Subcommands

SubcommandDescription
telaio migrate create <name>Create a new migration file
telaio migrate latestRun all pending migrations
telaio migrate upRun the next pending migration only
telaio migrate downRoll back the most recent applied migration
telaio migrate statusList all migrations with applied/pending status
pnpx telaio migrate create add-users-table
pnpx telaio migrate latest
pnpx telaio migrate status

Migration file format

telaio migrate create <name> generates a file named <timestamp>_<name>.ts in your migrations directory. The timestamp format is YYYYMMDDHHMMSS.

src/db/migrations/
  20240315120000_add-users-table.ts
  20240316090000_add-posts-table.ts
  20240317150000_add-indexes.ts

Each file must export up and down functions:

import type { Kysely } from 'kysely';
import { sql } from 'kysely';

export async function up(db: Kysely<unknown>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'uuid', (col) =>
      col.primaryKey().defaultTo(sql`gen_random_uuid()`)
    )
    .addColumn('email', 'text', (col) => col.notNull().unique())
    .addColumn('name', 'text', (col) => col.notNull())
    .addColumn('created_at', 'timestamptz', (col) =>
      col.notNull().defaultTo(sql`now()`)
    )
    .execute();
}

export async function down(db: Kysely<unknown>): Promise<void> {
  await db.schema.dropTable('users').execute();
}

Migration tables

Telaio uses two separate migration tables that never conflict with each other:

TablePurpose
_telaio_migrationsTelaio's own internal framework migrations
kysely_migrationYour application migrations (Kysely default)

telaio migrate latest runs framework migrations first, then your app migrations, in the correct dependency order. You do not need to manage framework migrations manually.

Customizing migration tables

You can control the PostgreSQL schema and table names for migration tracking via environment variables:

VariableDefaultDescription
DATABASE_MIGRATION_SCHEMApublicPostgreSQL schema for all migration tracking tables and framework DDL
DATABASE_MIGRATION_TABLEkysely_migrationTable name for your app migrations
DATABASE_MIGRATION_LOCK_TABLEkysely_migration_lockLock table name for your app migrations
# .env
DATABASE_MIGRATION_SCHEMA=app
DATABASE_MIGRATION_TABLE=my_migrations
DATABASE_MIGRATION_LOCK_TABLE=my_migrations_lock

When DATABASE_MIGRATION_SCHEMA is set, Telaio will:

  1. Create the schema if it does not exist (CREATE SCHEMA IF NOT EXISTS)
  2. Place all migration tracking tables (_telaio_migrations, your app migration table) in that schema
  3. Create the trigger_set_updated_at_timestamp() function in that schema

Once you set these values, you must keep them the same for the lifetime of the project. Changing them after migrations have run will cause Kysely to create new empty tracking tables and attempt to re-run all migrations.

When using a custom schema, user migrations that reference trigger_set_updated_at_timestamp() in CREATE TRIGGER statements must schema-qualify the function name. For example: "app".trigger_set_updated_at_timestamp().

Framework table names (_telaio_migrations, _telaio_migrations_lock) are not configurable.

Custom migrations directory

Pass --dir to override the default migrations directory:

pnpx telaio migrate latest --dir src/custom/migrations
pnpx telaio migrate create add-feature --dir src/custom/migrations

The default directory is resolved from your project root.

Running migrations programmatically

If you need to run migrations inside application code rather than from the CLI:

import { migrateToLatest } from 'telaio/db/migrations';

await migrateToLatest({
  db,
  migrationsDir: 'src/db/migrations',
});
// Returns { framework: MigrationResult[], user: MigrationResult[] }

migrateToLatest runs framework migrations first, then your app migrations — in the correct order automatically. You do not need to call runFrameworkMigrations separately.

On this page