Skip to main content
Migrations allow you to evolve your database schema over time. Kit uses SeaORM migrations with a Laravel-inspired workflow.
For CLI commands (kit migrate, kit migrate:rollback, etc.), see the CLI Migrations Reference.

Creating Migrations

Generate a new migration file:
kit make:migration create_users_table
This creates a timestamped migration file in the migrations/ directory:
migrations/
├── mod.rs
└── m20240115_120000_create_users_table.rs

Migration Structure

Every migration has two methods:
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    // Run the migration
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // Create tables, add columns, etc.
    }

    // Reverse the migration
    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // Drop tables, remove columns, etc.
    }
}

Schema Operations

Creating Tables

use sea_orm_migration::prelude::*;

async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .create_table(
            Table::create()
                .table(Users::Table)
                .if_not_exists()
                .col(
                    ColumnDef::new(Users::Id)
                        .integer()
                        .not_null()
                        .auto_increment()
                        .primary_key(),
                )
                .col(ColumnDef::new(Users::Email).string().not_null().unique_key())
                .col(ColumnDef::new(Users::Name).string().not_null())
                .col(ColumnDef::new(Users::PasswordHash).string().not_null())
                .col(ColumnDef::new(Users::CreatedAt).timestamp().not_null())
                .col(ColumnDef::new(Users::UpdatedAt).timestamp().not_null())
                .to_owned(),
        )
        .await
}

// Define the table and column identifiers
#[derive(DeriveIden)]
enum Users {
    Table,
    Id,
    Email,
    Name,
    PasswordHash,
    CreatedAt,
    UpdatedAt,
}

Dropping Tables

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .drop_table(Table::drop().table(Users::Table).to_owned())
        .await
}

Column Types

MethodDatabase TypeNotes
integer()INTEGER32-bit integer
big_integer()BIGINT64-bit integer
small_integer()SMALLINT16-bit integer
float()FLOATFloating point
double()DOUBLEDouble precision
decimal()DECIMALFixed-point
string()VARCHAR(255)Variable length string
string_len(n)VARCHAR(n)Custom length string
text()TEXTLong text
boolean()BOOLEANTrue/false
timestamp()TIMESTAMPDate and time
date()DATEDate only
time()TIMETime only
blob()BLOBBinary data
json()JSONJSON data
uuid()UUIDUUID type

Column Modifiers

ColumnDef::new(Column::Name)
    .string()
    .not_null()              // NOT NULL constraint
    .null()                  // Allows NULL (default)
    .default("value")        // Default value
    .unique_key()            // UNIQUE constraint
    .primary_key()           // PRIMARY KEY
    .auto_increment()        // AUTO_INCREMENT

Adding Columns

async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .alter_table(
            Table::alter()
                .table(Users::Table)
                .add_column(
                    ColumnDef::new(Users::PhoneNumber)
                        .string()
                        .null()
                )
                .to_owned(),
        )
        .await
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .alter_table(
            Table::alter()
                .table(Users::Table)
                .drop_column(Users::PhoneNumber)
                .to_owned(),
        )
        .await
}

Modifying Columns

async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .alter_table(
            Table::alter()
                .table(Users::Table)
                .modify_column(
                    ColumnDef::new(Users::Name)
                        .string_len(500)  // Change VARCHAR(255) to VARCHAR(500)
                        .not_null()
                )
                .to_owned(),
        )
        .await
}

Renaming Columns

async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .alter_table(
            Table::alter()
                .table(Users::Table)
                .rename_column(Users::Name, Users::FullName)
                .to_owned(),
        )
        .await
}

Indexes

Creating Indexes

async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .create_index(
            Index::create()
                .name("idx_users_email")
                .table(Users::Table)
                .col(Users::Email)
                .unique()  // Optional: make it unique
                .to_owned(),
        )
        .await
}

Composite Indexes

manager
    .create_index(
        Index::create()
            .name("idx_posts_user_created")
            .table(Posts::Table)
            .col(Posts::UserId)
            .col(Posts::CreatedAt)
            .to_owned(),
    )
    .await

Dropping Indexes

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .drop_index(Index::drop().name("idx_users_email").to_owned())
        .await
}

Foreign Keys

Adding Foreign Keys

async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
        .create_table(
            Table::create()
                .table(Posts::Table)
                .if_not_exists()
                .col(
                    ColumnDef::new(Posts::Id)
                        .integer()
                        .not_null()
                        .auto_increment()
                        .primary_key(),
                )
                .col(ColumnDef::new(Posts::UserId).integer().not_null())
                .col(ColumnDef::new(Posts::Title).string().not_null())
                .col(ColumnDef::new(Posts::Content).text().not_null())
                .foreign_key(
                    ForeignKey::create()
                        .name("fk_posts_user")
                        .from(Posts::Table, Posts::UserId)
                        .to(Users::Table, Users::Id)
                        .on_delete(ForeignKeyAction::Cascade)
                        .on_update(ForeignKeyAction::Cascade),
                )
                .to_owned(),
        )
        .await
}

Foreign Key Actions

ActionDescription
CascadeDelete/update child rows automatically
SetNullSet foreign key to NULL
SetDefaultSet foreign key to default value
RestrictPrevent delete/update if referenced
NoActionSimilar to Restrict

Migration Workflow

1. Create Migration

kit make:migration create_posts_table

2. Edit the Migration

// migrations/m20240115_130000_create_posts_table.rs
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(Posts::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Posts::Id)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(Posts::Title).string().not_null())
                    .col(ColumnDef::new(Posts::Body).text().not_null())
                    .col(ColumnDef::new(Posts::Published).boolean().not_null().default(false))
                    .col(ColumnDef::new(Posts::CreatedAt).timestamp().not_null())
                    .col(ColumnDef::new(Posts::UpdatedAt).timestamp().not_null())
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Posts::Table).to_owned())
            .await
    }
}

#[derive(DeriveIden)]
enum Posts {
    Table,
    Id,
    Title,
    Body,
    Published,
    CreatedAt,
    UpdatedAt,
}

3. Run the Migration

kit migrate

4. Sync Entities

kit db:sync
This generates the corresponding entity file in src/models/.

Best Practices

Always Write Down Migrations

Always implement down() to allow rollbacks:
// Good: Reversible migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager.create_table(/* ... */).await
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager.drop_table(/* ... */).await
}

Use Descriptive Names

# Good: Describes the change
kit make:migration add_email_verified_to_users
kit make:migration create_order_items_table
kit make:migration add_index_to_posts_slug

# Bad: Vague names
kit make:migration update_users
kit make:migration change_table

One Change Per Migration

Keep migrations focused on a single change:
# Good: Separate migrations
kit make:migration create_categories_table
kit make:migration add_category_id_to_posts

# Avoid: Multiple unrelated changes in one migration

Test Migrations Both Ways

Before committing, verify both directions work:
kit migrate           # Apply
kit migrate:rollback  # Rollback
kit migrate           # Apply again

CLI Commands Reference

CommandDescription
kit make:migration <name>Create a new migration
kit migrateRun all pending migrations
kit migrate:statusShow migration status
kit migrate:rollbackRollback last migration
kit migrate:rollback --step 3Rollback last 3 migrations
kit migrate:freshDrop all tables and re-run
kit db:syncSync schema to entities
See the CLI Migrations Reference for detailed command documentation.