Migrations
Schema changes live in the migrations crate — a product crate under
crates/, scaffolded by nestrs new alongside seed. Migrations are
SeaORM’s, so a migration is a struct
implementing MigrationTrait with an up and a down. Neither the API nor
the worker run migrations on startup; you run them explicitly with nestrs run db.
Write a migration
Section titled “Write a migration”nestrs g migration create_orgOne file per migration, named m<utc-date>_<seq>_<what>.rs.
DeriveMigrationName takes the version from the file name; a DeriveIden
enum names the table and its columns so there are no stringly-typed
identifiers. Fill in the columns the generator stubbed:
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(Org::Table) .if_not_exists() .col(ColumnDef::new(Org::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Org::Name).string().not_null().unique_key()) .to_owned(), ) .await }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .drop_table(Table::drop().table(Org::Table).to_owned()) .await }}
#[derive(DeriveIden)]enum Org { Table, Id, Name,}down is the inverse of up — it’s what nestrs run db down and nestrs run db fresh
replay to roll back cleanly.
Foreign keys
Section titled “Foreign keys”A later migration references an earlier table by adding a foreign key — the
user table hangs off org:
.col(ColumnDef::new(User::OrgId).uuid().not_null()).foreign_key( ForeignKey::create() .name("fk_user_org_id") .from(User::Table, User::OrgId) .to(Org::Table, Org::Id) .on_delete(ForeignKeyAction::Restrict) .on_update(ForeignKeyAction::Cascade),)Register it
Section titled “Register it”Two registrations, both written by g migration: the mod line in lib.rs,
and the ordered vec in migrator.rs. Migrations run top to bottom, so a table
must appear after anything it references.
mod m20260526_000000_create_org;mod m20260526_000001_create_user;mod migrator;
pub use migrator::{Migrator, migrate};migrator.rs is regenerated from that mod list on every
g migration, so the vec can never fall behind the files:
use sea_orm_migration::prelude::*;
use super::{m20260526_000000_create_org, m20260526_000001_create_user};
pub struct Migrator;
#[async_trait::async_trait]impl MigratorTrait for Migrator { fn migrations() -> Vec<Box<dyn MigrationTrait>> { vec![ Box::new(m20260526_000000_create_org::Migration), Box::new(m20260526_000001_create_user::Migration), ] }}Writing a migration by hand means writing both — the one you forget is the one that silently never runs.
Run them
Section titled “Run them”nestrs run db up # apply every pending migrationnestrs run db down # roll back the last applied migrationnestrs run db status # show applied vs. pendingnestrs run db fresh # drop every table, then re-apply from scratchEach verb shells out to the crate’s migrate binary at
crates/migrations/src/bin/migrate.rs. It connects through
nest_rs_seaorm::connect_from_env() — the single connector for tools
outside the DI container, which resolves NESTRS_DATABASE__* through the same
.env cascade the apps use. Reaching for std::env::var("NESTRS_DATABASE__URL")
in a tool of your own works only when the variable is really in the process
environment; connect_from_env works either way.
Going further
Section titled “Going further”Built by YV17labs