# Adding a database migration

> Create, apply and review EF Core migrations against the AppDbContext, and how migrations run on startup.

## Where migrations live

Schema is server-side only, so this is an API-only task with no frontend counterpart. EF Core
migrations live in `api/src/Slicekit.Core/Persistence/Migrations/`. The `DbContext` is
`AppDbContext`, and the startup project that supplies the design-time configuration is
`Slicekit.Api`. Both are required arguments to every `dotnet ef` command.

## 1. Create the migration

From `api/`, run the generator and point it at the context and startup project:

```sh
dotnet ef migrations add  \
  --project src/Slicekit.Core \
  --startup-project src/Slicekit.Api \
  --context AppDbContext
```

Use PascalCase, action-first names that describe the change: `AddUserDisplayName`,
`DropRefreshTokenUserAgent`, `AddConsentsSoftDeleteEncryption`. EF writes a
`<timestamp>_.cs` file (plus its designer partial) into the `Migrations/` folder.

## 2. How migrations are applied

You never run a manual `database update` in normal development. `ApplyMigrationsAsync` in
`api/src/Slicekit.Core/Configuration/Database.cs` calls `db.Database.MigrateAsync()` on the host
before the app starts serving, then syncs permissions:

```csharp
public static async Task ApplyMigrationsAsync(this IHost host)
{
    using var scope = host.Services.CreateScope();
    var db = scope.ServiceProvider.GetRequiredService();
    await db.Database.MigrateAsync();

    var permissionSync = scope.ServiceProvider.GetRequiredService();
    await permissionSync.SyncAsync();
}
```

That single path covers three environments:

- **Local dev**: applied at API startup. Add the migration, then just run the API.
- **Tests**: Testcontainers spins up a clean Postgres per fixture and applies every migration from
  scratch, so a broken migration fails the suite.
- **CI and production**: applied at startup. You can also run a separate `dotnet ef database update`
  step in your pipeline if you prefer to gate deploys on it. For zero-downtime deploys, prefer
  migrations that are forward-compatible with the previously deployed code.

## 3. Order changes around the outbox

Wolverine uses a transactional outbox in Postgres (see [CQRS and events](/docs/cqrs-and-events)).
Migrations that touch outbox-adjacent tables or sequences must land **before** the new code rolls
out, so in-flight outbox rows still match a schema the workers can deserialize.

The safe additive pattern:

1. Add a migration with additive-only changes (new columns nullable, new tables).
2. Deploy. The old code keeps running, but the schema is ready.
3. Land the code change that uses the new shape.
4. If needed, add a second migration that tightens the schema (for example makes the column
   `NOT NULL`) once all running code populates it.

For destructive changes (drop a column, drop a table), reverse the order: stop writing the value
first, deploy, then drop.

## 4. Forward-only is the policy

If a migration that already shipped to `main` turns out wrong, add a new "undo" migration rather
than reaching for `dotnet ef migrations remove` after the fact. `remove` rewrites history and
breaks anyone who already pulled `main`. It is only acceptable on a branch that has not been
merged yet.

## Custom SQL

Use the migration's `migrationBuilder.Sql(...)` for things EF cannot model (functions, triggers,
partial indexes). Quote identifiers (`"User"`, not `User`): Postgres folds unquoted identifiers to
lowercase.

```csharp
migrationBuilder.Sql("""
    CREATE INDEX ix_user_active_email ON "User" ("Email") WHERE "DeletedAt" IS NULL;
""");
```

## Configure entities, do not annotate them

Do not put EF attributes on domain types. Each aggregate keeps its persistence mapping in an
`IEntityTypeConfiguration` under `api/src/Slicekit.Core/Persistence/Configurations/`
(`UserConfiguration`, `RefreshTokenConfiguration`, and so on). `AppDbContext.OnModelCreating`
already discovers them:

```csharp
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
```

Adding a configuration file is enough to change the model; the next `migrations add` picks up the
diff. This keeps the domain free of EF concerns, a rule enforced by the
`Domain_Must_Not_Depend_On` architecture test. The same split
between domain and persistence drives the rest of the backend; see
[Vertical slices](/docs/vertical-slices) and [Project structure](/docs/project-structure).

## Verify

- `dotnet build api/slicekit.slnx`: passes, so the new migration compiles.
- `dotnet test api/slicekit.slnx --nologo`: passes. Feature and API tests apply all migrations against
  a fresh Postgres container, so a broken migration surfaces here.
- Open `api/src/Slicekit.Core/Persistence/Migrations/<timestamp>_.cs` and read the generated
  `Up` and `Down`: the SQL should match what you intended.

## Conventions to keep

- **Name action-first in PascalCase.** `AddUserDisplayName`, not `Migration3`.
- **Additive before destructive.** Add nullable, deploy, backfill, then tighten or drop.
- **Forward-only on `main`.** Fix a bad migration with a new one, never `remove` after merge.
- **Map in configurations, not attributes.** Mappings live under `Persistence/Configurations/`.
- **Quote Postgres identifiers** in any raw `migrationBuilder.Sql(...)`.
