Skip to content
Back to skills

Typeorm Migrations

ASecurity

Creating TypeORM database migrations. Use when schema changes are needed (new columns, tables, indexes, constraints).

  • 33 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 20, 2026
databasestypescriptgobashsqlexpressdockergitdatabasebackend

Works with

  • mcp

Security analysis

A100/100

Scanned September 20, 2026

npx -y skills add ayunis-core/ayunis-core --skill typeorm-migrations --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Typeorm Migrations?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Typeorm Migrations
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ayunis-core-typeorm-migrations/badge)](https://www.skillsdirectory.com/skills/ayunis-core-typeorm-migrations)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: typeorm-migrations
description: Creating TypeORM database migrations. Use when schema changes are needed (new columns, tables, indexes, constraints).
---

# TypeORM Database Migrations

## Golden Rule

**Never write migration files by hand.** Always auto-generate them from TypeORM entity changes. Never edit a generated migration — if it's wrong, fix the entity and regenerate.

## Entity Rules — The Source of Truth

TypeORM entities (Records) are the **single source of truth** for the database schema. Everything must be expressed through decorators. If a constraint, index, or FK exists in the database but not in an entity decorator, TypeORM will try to drop it on the next `migration:generate`.

### Relations define constraints, not loading behavior

`@ManyToOne` creates a **foreign key constraint** in the database. It does NOT mean the relation is eagerly loaded — relations are lazy by default. You control loading per-query via `relations: { ... }`.

**Always define `@ManyToOne` when a column references another table**, even if you never load the relation object:

```typescript
// CORRECT — column + relation = FK constraint in DB
@Column({ name: 'integration_id', type: 'varchar' })
integrationId: string;

@ManyToOne(() => McpIntegrationRecord, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'integration_id' })
integration: McpIntegrationRecord;

// WRONG — plain column = no FK, no referential integrity
@Column({ name: 'integration_id', type: 'varchar' })
integrationId: string;
```

### All constraints must use decorators

| DB concept | Decorator |
| --- | --- |
| Foreign key | `@ManyToOne` / `@OneToOne` + `@JoinColumn` |
| Unique constraint (full) | `@Unique([...])` on class |
| Unique constraint (partial) | `@Index([...], { unique: true, where: '...' })` on class |
| Check constraint | `@Check('name', 'expression')` on class |
| Index | `@Index()` on property or `@Index([...])` on class |
| Enum column | `@Column({ type: 'enum', enum: MyEnum })` |

**Partial unique indexes** (with `WHERE` clause) cannot use `@Unique` — use `@Index` with `unique: true` and `where` instead.

If you add a value to a TypeScript enum used in `@Column({ type: 'enum' })`, you **must** generate a migration.

### Never hand-write constraint names

TypeORM generates deterministic constraint/index names. Hand-written names (e.g. `FK_my_custom_name`) will mismatch, causing perpetual drift.

## Workflow

### 1. Modify the Entity

Change the record file. Extend `BaseRecord` for `id`, `createdAt`, `updatedAt`:

```typescript
@Entity('agents')
export class AgentRecord extends BaseRecord {
  @Column({ type: 'varchar', length: 255, nullable: true })
  marketplaceSlug: string | null;
}
```

### 2. Generate the Migration

The dev stack must be running (`./dev status`).

```bash
pnpm run migration:generate:dev src/db/migrations/DescriptiveMigrationName
```

> **No `--` before the path.** pnpm forwards the trailing path straight to the script. Adding a `--` separator makes pnpm swallow the path, so TypeORM sees zero arguments and aborts with `Nicht genügend Argumente ohne Optionen: 0 vorhanden, mindestens 1 benötigt` (`ELIFECYCLE Command failed with exit code 1`). Pass the path with no `--`.

### 3. Review the Generated Migration

- Contains **only** the expected changes (no unrelated drift)
- `up()` and `down()` are symmetric
- No hand-written constraint or index names

If the migration contains unexpected drift, the local DB is out of sync. **Rebuild it — do not hand-author a migration to reconcile.** See "Fixing Migration Drift" below.

### 4. Run the Migration

```bash
pnpm run migration:run:dev
```

### 5. Verify Zero Drift

```bash
pnpm run migration:generate:dev src/db/migrations/VerifyNoDrift
```

This **must** print `No changes in database schema were found`. If it generates a file, entities and migrations are still out of sync — investigate before committing.

### 6. Validate

```bash
pnpm exec tsc --noEmit
pnpm run test
```

## Naming Convention

PascalCase describing the change:

| Change | Migration Name |
| --- | --- |
| Add a column | `AddMarketplaceSlugToAgents` |
| Create a table | `CreateTeamSharesTable` |
| Add an index | `AddIndexOnThreadCreatedAt` |
| Remove a column | `RemoveUserShareScope` |
| Add a constraint | `CascadeDeleteSharesOnScopeDelete` |

## Fixing Migration Drift

"Drift" here means: entities on the branch, the migration files, and the local Postgres schema no longer agree. Typical triggers:

- Switching between branches whose migration sets diverge (slot's Postgres volume was written by branch A, you're now on branch B).
- `pnpm run migration:generate:dev` produces surprise changes (removes/renames on things you never touched, or resurrects columns).
- Backend refuses to start with errors like `error: relation "<table>" already exists` (PG code `42P07`) or `column "..." of relation "..." does not exist`.

**Rule: rebuild the database from scratch. Do not hand-author a reconciliation migration.** Hand-written reconciliation migrations poison the migration history for every teammate and every future deploy — they are worse than the drift they were meant to fix.

### The rebuild

Shut the stack down and spin the DB up on an empty volume, then let migrations replay from zero:

```bash
# 1. Shut the stack down completely (from any worktree)
./dev down

# 2. Wipe the slot's Postgres volume so ./dev up starts from an empty DB.
#    Without -v the volume survives and the drift returns immediately.
docker compose -p ayunis-dev-<SLOT> down -v

# 3. Bring the stack back up — ./dev up replays every migration from scratch
./dev up --slot <SLOT>

# 4. Optional: reseed fixtures (see seed-database skill)
cd ayunis-core-backend && pnpm run seed:minimal:ts
```

`<SLOT>` is the slot number this worktree uses — the same one you passed to `./dev up`. See the `dev-environment` skill for slot conventions.

### After the rebuild

Verify zero drift before making further changes:

```bash
cd ayunis-core-backend
pnpm run migration:generate:dev src/db/migrations/VerifyNoDrift
# Must print: "No changes in database schema were found"
```

If a `VerifyNoDrift` file *is* produced, entities and migrations are genuinely out of sync — that's a legitimate migration to *generate* (via TypeORM), not to hand-write. Investigate which entity changed and, if the fix belongs in an earlier migration, edit *that* migration's entity source, not the reconciliation file.

## Anti-Patterns

| Don't | Why | Instead |
| --- | --- | --- |
| Write migration SQL by hand | Drift between entities and schema | Modify the entity, then auto-generate |
| Hand-author a migration to reconcile drift | Poisons migration history for teammates and deploys | Rebuild the DB (`./dev down` + wipe volume + `./dev up`) |
| Edit a generated migration | Constraint names will mismatch | Fix the entity and regenerate |
| Hand-write FK/index/constraint names | Perpetual drift on future generates | Let TypeORM name everything |
| Use `@Column` for a FK without `@ManyToOne` | No FK in the database, no referential integrity | Add `@ManyToOne` + `@JoinColumn` |
| Add enum values without a migration | DB enum won't match TypeScript enum | Generate a migration after adding values |
| Use `@Unique` for partial constraints | Produces full constraint, not partial | Use `@Index({ unique: true, where: '...' })` |
| Skip zero-drift verification | Entities and migrations may still be misaligned | Always verify after running |
| Commit without running | May fail at runtime | Always `migration:run:dev` first |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…