3.5.8. Migrations
In this chapter, you'll learn what a migration is and how to generate a migration or write it manually.
What is a Migration?#
A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.
The migration's file has a class with two methods:
- The
upmethod reflects changes on the database. - The
downmethod reverts the changes made in theupmethod.
Generate Migration#
Instead of you writing the migration manually, the Medusa CLI tool provides a db:generate command to generate a migration for a modules' data models.
For example, assuming you have a blog Module, you can generate a migration for it by running the following command:
This generates a migration file under the migrations directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in this section.
Write a Migration Manually#
You can also write migrations manually. To do that, create a file in the migrations directory of the module and in it, a class that has an up and down method. The class's name should be of the format Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts to ensure migrations are ran in the correct order.
For example:
@medusajs/framework package. If you're using an older version of Medusa, change the import statement to @mikro-orm/migrations.1import { Migration } from "@medusajs/framework/mikro-orm/migrations"2 3export class Migration202507021059 extends Migration {4 5 async up(): Promise<void> {6 this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));")7 }8 9 async down(): Promise<void> {10 this.addSql("drop table if exists \"author\" cascade;")11 }12 13}
The migration class in the file extends the Migration class imported from @medusajs/framework/mikro-orm/migrations. In the up and down method of the migration class, you use the addSql method provided by MikroORM's Migration class to run PostgreSQL syntax.
In the example above, the up method creates the table author, and the down method drops the table if the migration is reverted.
Migration File Naming#
Migrations are executed in the ascending order of their file names. So, it's recommended to prefix the migration file name with the timestamp of when the migration was created. This ensures that migrations are executed in the order they were created.
For example, if you create a migration on July 2, 2025, at 10:59 AM, the file name should be Migration202507021059_create_brand.ts. This way, the migration will be executed after any previous migrations that were created before this date and time.
Run the Migration#
To run your migration, run the following command:
--skip-links option.This reflects the changes in the database as implemented in the migration's up method.
Rollback the Migration#
To rollback or revert the last migration you ran for a module, run the following command:
This rolls back the last ran migration on the Blog Module.
Caution: Rollback Migration before Deleting#
If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations.
For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors.
So, always rollback the migration before deleting it.
More Database Commands#
To learn more about the Medusa CLI's database commands, refer to this CLI reference.
Data Migration Scripts#
In some use cases, you may need to perform data migration after updates to the database. For example, after you added a Site data model to the Blog Module, you want to assign all existing posts and authors to a default site. Another example is updating data stored in a third-party system.
In those scenarios, you can instead create a data migration script. They are asynchronous function that the Medusa application executes once when you run the npx medusa db:migrate command.
How to Create a Data Migration Script#
You can create data migration scripts in a TypeScript or JavaScript file under the src/migration-scripts directory. The file must export an asynchronous function that will be executed when the db:migrate command is executed.
For example, to create a data migration script for the Blog Module example, create the file src/migration-scripts/migrate-blog-data.ts with the following content:
1import { MedusaModule } from "@medusajs/framework/modules-sdk"2import { ExecArgs } from "@medusajs/framework/types"3import { BLOG_MODULE } from "../modules/blog"4import { createWorkflow } from "@medusajs/framework/workflows-sdk"5 6export default async function migrateBlogData({ container }: ExecArgs) {7 // Check that the blog module exists8 if (!MedusaModule.isInstalled(BLOG_MODULE)) {9 return10 }11 12 await migrateBlogDataWorkflow(container).run({})13}14 15const migrateBlogDataWorkflow = createWorkflow(16 "migrate-blog-data",17 () => {18 // Assuming you have these steps19 createDefaultSiteStep()20 21 assignBlogDataToSiteStep()22 }23)
In the above example, you default export an asynchronous function that receives an object parameter with the Medusa Container property.
In the function, you first ensure that the Blog Module is installed to avoid errors otherwise. Then, you run a workflow that you've created in the same file that performs the necessary data migration.
Test Data Migration Script
To test out the data migration script, run the migration command:
Medusa will run any pending migrations and migration scripts, including your script.
If the script runs successfully, Medusa won't run the script again.
If there are errors in the script, you'll receive an error in the migration script logs. Medusa will keep running the script every time you run the migration command until it runs successfully.
Migration Examples#
The following section provides examples of writing migrations for common database changes.
Example: Migration with Relationship#
Consider you have a module with two data models: Author and Post. An author can have multiple posts, so there's a one-to-many relationship between the two models:
1import { model } from "@medusajs/framework/utils"2 3const Post = model.define("post", {4 id: model.id().primaryKey(),5 title: model.text(),6 author: model.belongsTo(() => Author, {7 mappedBy: "posts"8 })9})10 11const Author = model.define("author", {12 id: model.id().primaryKey(),13 name: model.text(),14 posts: model.hasMany(() => Post, {15 mappedBy: "author"16 })17})
To create a migration that reflects this relationship in the database, you can create a migration file as follows:
1import { Migration } from "@medusajs/framework/mikro-orm/migrations";2 3export class Migration20251230112505 extends Migration {4 5 override async up(): Promise<void> {6 this.addSql(`create table if not exists "author" ("id" text not null, "name" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "author_pkey" primary key ("id"));`);7 this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_author_deleted_at" ON "author" ("deleted_at") WHERE deleted_at IS NULL;`);8 9 this.addSql(`create table if not exists "post" ("id" text not null, "title" text not null, "author_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "post_pkey" primary key ("id"));`);10 this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_author_id" ON "post" ("author_id") WHERE deleted_at IS NULL;`);11 this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_deleted_at" ON "post" ("deleted_at") WHERE deleted_at IS NULL;`);12 13 this.addSql(`alter table if exists "post" add constraint "post_author_id_foreign" foreign key ("author_id") references "author" ("id") on update cascade;`);14 }15 16 override async down(): Promise<void> {17 this.addSql(`alter table if exists "post" drop constraint if exists "post_author_id_foreign";`);18 19 this.addSql(`drop table if exists "author" cascade;`);20 21 this.addSql(`drop table if exists "post" cascade;`);22 }23 24}
In this migration, the up method creates the author and post tables, including the foreign key constraint that establishes the relationship between them. The down method removes the foreign key constraint and drops both tables.
Example: Migration to Add a New Column#
Consider you have an existing Post data model, and you added a new published_at column to it.
You can create a migration file to add this new column as follows:
1import { Migration } from "@medusajs/framework/mikro-orm/migrations";2 3export class Migration20251230113025 extends Migration {4 5 override async up(): Promise<void> {6 this.addSql(`alter table if exists "post" add column if not exists "published_at" timestamptz null;`);7 }8 9 override async down(): Promise<void> {10 this.addSql(`alter table if exists "post" drop column if exists "published_at";`);11 }12 13}
In this migration, the up method adds the published_at column to the post table, while the down method removes it.
Example: Migration to Remove a Column#
Consider you have an existing Post data model, and you removed the published_at column from it.
You can create a migration file to remove this column as follows:
1import { Migration } from "@medusajs/framework/mikro-orm/migrations";2 3export class Migration20251230113125 extends Migration {4 5 override async up(): Promise<void> {6 this.addSql(`alter table if exists "post" drop column if exists "published_at";`);7 }8 9 override async down(): Promise<void> {10 this.addSql(`alter table if exists "post" add column if not exists "published_at" timestamptz null;`);11 }12 13}
In this migration, the up method removes the published_at column from the post table, while the down method adds it back.
Example: Migration to Rename a Column#
Consider you have an existing Post data model with a title column, and you renamed it to headline.
You can create a migration file to rename this column as follows:
1import { Migration } from "@medusajs/framework/mikro-orm/migrations";2 3export class Migration20251230113214 extends Migration {4 5 override async up(): Promise<void> {6 this.addSql(`alter table if exists "post" rename column "title" to "headline";`);7 }8 9 override async down(): Promise<void> {10 this.addSql(`alter table if exists "post" rename column "headline" to "title";`);11 }12 13}
In this migration, the up method renames the title column to headline in the post table, while the down method renames it back to title.
Example: Migration to Create an Index#
Consider you have an existing Post data model, and add an index on the headline column to improve query performance:
You can create a migration file to create this index as follows:
1import { Migration } from "@medusajs/framework/mikro-orm/migrations";2 3export class Migration20251230113322 extends Migration {4 5 override async up(): Promise<void> {6 this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_headline" ON "post" ("headline") WHERE deleted_at IS NULL;`);7 }8 9 override async down(): Promise<void> {10 this.addSql(`drop index if exists "IDX_post_headline";`);11 }12 13}
In this migration, the up method creates an index on the headline column of the post table, while the down method removes the index.
Example: Migration to Drop an Index#
Consider you have an existing Post data model with an index on the headline column, and you decide to remove this index.
You can create a migration file to drop this index as follows:
1import { Migration } from "@medusajs/framework/mikro-orm/migrations";2 3export class Migration20251230113350 extends Migration {4 5 override async up(): Promise<void> {6 this.addSql(`drop index if exists "IDX_post_headline";`);7 }8 9 override async down(): Promise<void> {10 this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_headline" ON "post" ("headline") WHERE deleted_at IS NULL;`);11 }12 13}
In this migration, the up method drops the index on the headline column of the post table, while the down method recreates the index.