Skip to main content

Migrations & Seeds

Every time you need to change the database schema — add a table, add a column, modify a constraint — you write a migration. Every time you need consistent starting data (default roles, plans, admin users), you write a seed. Both use the same TypeScript file format and are run via Makefile commands inside Docker.

The file format

Both migrations and seeds export two functions: up (apply the change) and down (reverse it). They receive Sequelize’s queryInterface via the context object:
A few conventions to follow:
  • Column names use snake_case — that’s what Sequelize maps to camelCase in models.
  • Always include created_at and updated_at on new tables.
  • Always write a down function, even if it just drops the table. You’ll thank yourself when you need to roll back.

Make commands

These all run inside the Docker container — Sequelize needs a live Postgres connection to work, and the container has it.

Adding a new table (step by step)

  1. Generate the migration file
    This creates a timestamped file in apps/backend/src/tools/rds/sequelize/migrations/. Open it and fill in the up and down functions.
  2. Write the migration Use queryInterface.createTable in up and queryInterface.dropTable in down:
  3. Run the migration
  4. Create a Sequelize model Add a new file in apps/backend/src/tools/rds/sequelize/models/widget.ts following the pattern of existing models.
  5. Register the model Add it to the models index in apps/backend/src/tools/rds/sequelize/models/index.ts.
  6. Verify
    The globalSetup in Jest runs migrate-up automatically before tests, so your new table will be present in the test database.

Adding a column to an existing table

For schema changes to existing tables, use addColumn / removeColumn:
⚠️ Watch out: if you’re adding a NOT NULL column to a table that already has rows, you need to either provide a defaultValue or do it in two steps: add the column as nullable, backfill data, then add a NOT NULL constraint. Doing it in one step on a table with existing data will fail.

Seeds

Seeds are for data that should always exist — default roles, product plans, an initial admin user. They follow the same up / down format:
For new catalog work, use paymentProcessor.createProduct rather than hand-written seeds — it creates the Stripe Product + Price and persists both products and prices rows. See Billing — Product vs Price. Seeds run in timestamp order, just like migrations. make fresh-start runs both migrate-up and seed-up in sequence.
Operational one-offs are different. Schema migrations and seeds are not the place for targeted backfills or provider reconciliation. Use Operational Scripts for those CLIs (docker compose exec backend pnpx ts-node … locally, node apps/backend/dist/scripts/… in production).

What’s next?

  • Sequelize tooling — how the Sequelize model layer is set up.
  • Monorepo — workspace structure and how packages reference each other.
  • Testing — how Jest’s globalSetup automatically applies migrations before tests.