How to manage database schema changes safely.
The TeachLink backend uses TypeORM migrations for schema management. Migration files are standard TypeORM MigrationInterface classes located in src/migrations/.
There are two mechanisms for schema updates:
- TypeORM
synchronize(development only — auto-creates tables from entities) - Explicit migration files (all environments — controlled, versioned changes)
Migration files live in src/migrations/ and follow the naming convention:
<TIMESTAMP>-<Description>.ts
Each file exports a class implementing MigrationInterface with two methods:
up(queryRunner)— applies the schema changedown(queryRunner)— reverses the schema change
Example (src/migrations/1630000000000-CreateMessageTable.ts):
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
export class CreateMessageTable1630000000000 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'messages',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'senderId', type: 'uuid', isNullable: false },
{ name: 'recipientId', type: 'uuid', isNullable: false },
{ name: 'content', type: 'text', isNullable: false },
{ name: 'createdAt', type: 'timestamptz', default: 'now()' },
{ name: 'readAt', type: 'timestamptz', isNullable: true },
],
}),
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('messages');
}
}| File | Description |
|---|---|
1630000000000-CreateMessageTable.ts |
Creates messages table with sender/recipient FKs |
1680000000000-create-schema-version-and-change-tables.ts |
Creates schema_version and schema_change tables |
1685000001000-add-currency-and-location-fields-to-users.ts |
Adds currency/location to users |
1685000001001-add-currency-field-to-courses.ts |
Adds currency to courses |
1748600000000-add-course-bulk-operations.ts |
Adds course bulk operations support |
1748700000000-add-grading-system.ts |
Adds grading system tables |
1748800000000-add-gamification-tiers.ts |
Adds gamification tier tables |
1762000000000-create-audit-log-table.ts |
Creates audit_log table |
AddTimezoneLocalePreferences.ts |
Adds timezone/locale preferences |
src/achievements/migrations/1700000000000-CreateAchievementsSchema.ts |
Creates achievements schema |
# Start the server first
pnpm start:dev
# In another terminal, run pending migrations
curl -X POST http://localhost:3000/migrations/run
# Check migration status
curl http://localhost:3000/migrationsOr via npm scripts:
pnpm migrate:run # Run all pending
pnpm migrate:status # Check status# Build the project first
pnpm build
# Run migrations using TypeORM CLI
npx typeorm-ts-node-commonjs migration:run -d src/config/datasource.tsIn development (NODE_ENV=development), TypeORM's synchronize: true is enabled. This means:
- Tables are auto-created from entity definitions on server startup
- You do NOT need to run migrations for schema changes during active development
- This is fast for prototyping but provides no version tracking
Important: When
synchronizeis on, running explicit migrations may fail with "relation already exists" because tables are already created. In that case, either:
- Disable synchronize (
NODE_ENV=productionor editdatabase.config.ts)- Drop tables first, then run migrations
curl -X POST http://localhost:3000/migrations/rollback
# or
pnpm migrate:rollback# Roll back last 3
curl -X POST http://localhost:3000/migrations/rollback/3
# or
COUNT=3 pnpm migrate:rollback:countcurl -X POST http://localhost:3000/migrations/rollback/to/002-create-courses-table
# or
MIGRATION_NAME=002-create-courses-table pnpm migrate:rollback:tocurl -X DELETE http://localhost:3000/migrations/reset
# or
pnpm migrate:reset
⚠️ Never run reset in production. It drops all managed tables.
- Create a new file in
src/migrations/:
# Naming convention: <timestamp>-<kebab-case-description>.ts
touch src/migrations/$(date +%s%N | cut -b1-13)-add-bio-to-users.ts- Implement the migration class:
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddBioToUsers<TIMESTAMP> implements MigrationInterface {
name = 'AddBioToUsers<TIMESTAMP>';
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({ name: 'bio', type: 'text', isNullable: true }),
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'bio');
}
}- Build and run:
pnpm build
# Restart server or run migration| Practice | Why |
|---|---|
Always implement down() |
Enables safe rollback |
| Never modify an applied migration | Create a new migration instead |
| Test rollbacks locally | Run up → verify → down → verify |
Use IF EXISTS / IF NOT NULL |
Makes migrations idempotent |
| Backup database before staging/prod migrations | Safety net |
| Keep migrations small and focused | Easier to review and rollback |
| Use timestamp-based naming | Ensures deterministic ordering |
| Error | Cause | Fix |
|---|---|---|
relation already exists |
Table created by synchronize or a prior migration |
Drop the table or disable synchronize |
column "X" of relation "Y" already exists |
Duplicate migration | Create a new migration to handle the state |
Cannot roll back: later migrations depend |
Dependency chain | Roll back later migrations first |
migration:run returns 404 |
Migration endpoints not wired | Check if endpoints exist; use synchronize for dev |
| Foreign key violation during migration | Data integrity issue | Clean data, then retry |
| Environment | synchronize |
Migrations |
|---|---|---|
| Development | true (default) |
Optional (synchronize handles schema) |
| Test | true |
Run before test suite |
| Staging | false |
Run manually after deployment |
| Production | false |
Run manually with backup |
Seed data is available for specific modules:
- Achievements:
src/achievements/achievements.seed.ts— seed achievement definitions
To run seeds, execute the seed function (typically exposed via an API endpoint or called during module initialization).
- Setup guide — how to get the database running
- Troubleshooting guide — database connection issues
- Database config — connection settings