Migrations
Schema migrations with SeaORM
Migrations are versioned schema changes written in Rust. Instead of running raw SQL against your database, you define each change as a struct with an up method (apply) and a down method (rollback). Rapina tracks which migrations have already run, so when your app starts it only applies the new ones.
This page walks through the full workflow: generating a migration, writing the schema change, and wiring it into your app.
Prerequisites
Your Cargo.toml needs the database feature and a database driver:
[dependencies]
rapina = { version = "0.8", features = ["sqlite"] }Replace sqlite with postgres or mysql depending on your database. You also need a database connection configured in your app — see the Database page if you haven't set that up yet.
Generating a Migration
Run the CLI from your project root:
rapina migrate new create_usersThis creates src/migrations/ if it doesn't exist, generates a timestamped migration file, and updates mod.rs to register it:
✓ Created src/migrations/
✓ Created src/migrations/m20260305_143022_create_users.rs
✓ Updated src/migrations/mod.rsThe name must be lowercase with underscores only — no hyphens, no uppercase. The timestamp prefix is added automatically.
Writing the Migration
Open the generated file. It starts as a skeleton with todo!() placeholders:
use rapina::sea_orm_migration;
use rapina::migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
todo!("Write your migration here")
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
todo!("Write your rollback here")
}
}Replace the todo!() calls with your schema changes. Here's a complete migration that creates a users table:
use rapina::sea_orm_migration;
use rapina::migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Email,
Name,
}
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Users::Table)
.col(
ColumnDef::new(Users::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Users::Email).string().not_null())
.col(ColumnDef::new(Users::Name).string().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await
}
}The DeriveIden enum defines your table and column names. The first variant is always Table (the table name), and the rest are columns. SeaORM converts variant names to snake_case — Email becomes email, CreatedAt becomes created_at.
The up method creates the table. The down method drops it. Always write both so migrations are reversible.
The mod.rs File
When you run rapina migrate new, the CLI also creates or updates src/migrations/mod.rs. This file declares your migration modules and registers them with the migrations! macro:
mod m20260305_143022_create_users;
rapina::migrations! {
m20260305_143022_create_users,
}The macro generates a Migrator struct that knows about all your migrations. As you add more, the CLI appends them:
mod m20260305_143022_create_users;
mod m20260306_091500_add_posts;
rapina::migrations! {
m20260305_143022_create_users,
m20260306_091500_add_posts,
}Order matters — migrations run top to bottom.
Running Migrations
Chain .run_migrations() after .with_database() in your app builder. Pending migrations run before the server starts listening:
use rapina::prelude::*;
use rapina::database::DatabaseConfig;
mod migrations;
#[tokio::main]
async fn main() -> std::io::Result<()> {
Rapina::new()
.with_database(DatabaseConfig::new("sqlite://app.db?mode=rwc"))
.await?
.run_migrations::<migrations::Migrator>()
.await?
.discover()
.listen("127.0.0.1:3000")
.await
}The turbofish ::<migrations::Migrator> points to the struct generated by the migrations! macro. If you forget to call .with_database() first, run_migrations returns an error.
Migrations that have already been applied are skipped. Only new ones run.
Adding More Migrations
Each schema change gets its own migration. Never edit a migration that has already been applied to a database — create a new one instead:
rapina migrate new add_bio_to_usersThe CLI appends the new module to mod.rs automatically. Open the generated file, write your ALTER TABLE logic in up, and the reverse in down.