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
| Subcommand | Description |
|---|---|
telaio migrate create <name> | Create a new migration file |
telaio migrate latest | Run all pending migrations |
telaio migrate up | Run the next pending migration only |
telaio migrate down | Roll back the most recent applied migration |
telaio migrate status | List all migrations with applied/pending status |
pnpx telaio migrate create add-users-table
pnpx telaio migrate latest
pnpx telaio migrate statusMigration 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.tsEach 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:
| Table | Purpose |
|---|---|
_telaio_migrations | Telaio's own internal framework migrations |
kysely_migration | Your 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:
| Variable | Default | Description |
|---|---|---|
DATABASE_MIGRATION_SCHEMA | public | PostgreSQL schema for all migration tracking tables and framework DDL |
DATABASE_MIGRATION_TABLE | kysely_migration | Table name for your app migrations |
DATABASE_MIGRATION_LOCK_TABLE | kysely_migration_lock | Lock table name for your app migrations |
# .env
DATABASE_MIGRATION_SCHEMA=app
DATABASE_MIGRATION_TABLE=my_migrations
DATABASE_MIGRATION_LOCK_TABLE=my_migrations_lockWhen DATABASE_MIGRATION_SCHEMA is set, Telaio will:
- Create the schema if it does not exist (
CREATE SCHEMA IF NOT EXISTS) - Place all migration tracking tables (
_telaio_migrations, your app migration table) in that schema - 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/migrationsThe 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.