# Branches and migrations

This page explains how to evolve your database schema without surprises: apply a migration after rehearsing it on the real database, and read every night the drift between the schema your repository declares and the database that is running. It also states where database branches stand: planned, they are not available yet.

## Database branches: not available yet

The price grid plans database branches, one copy of the database per repository branch (1 on Discovery, 3 on Standard, 25 on Pro, 50 on Sovereign). They are not served yet: no screen, no tool and no operation creates or lists a branch.

What that means today:

- **Every environment class shares the same database.** A preview, staging and production all receive the same `DATABASE_URL` (see [Database](database.md)). A migration run from a preview therefore touches the production database.
- **To try a schema on its own**, create a second project with its own database (`project_create_with_backend`), or set `DATABASE_URL` yourself on the preview environment, pointing to another database: your variable wins over Bastide's.

> **Attention.** Do not run `prisma migrate deploy` or an equivalent when a preview starts: it would change the production database.

## Applying a schema migration

A migration always takes two passes, in this order:

1. **The rehearsal.** The migration is run on the real database, inside a transaction that is **rolled back**: same schema, same rows, same locks, and nothing remains. The platform measures its duration, the locks it takes (the strongest mode per relation) and, when it is certain, its inverse migration.
2. **The application.** The migration is run again and committed. What is returned is the measurement of that second pass: the database may have changed in between.

What is refused:

- **A destructive effect**: a table dropped or truncated, a column removed (`migration_destructive`). That goes through the SQL editor in write mode, which has it confirmed and retyped (see [Database](database.md)).
- **Transaction control and session state** in the text (`BEGIN`, `COMMIT`, `SET`, `COPY`, cursors, `set_config`): otherwise the migration would commit its own rehearsal.
- **More than 200 statements**: split the migration into several calls.
- **A statement the database refuses** (`migration_refusee_par_la_base`): nothing is applied; the detail names the statement and its SQL state.

The inverse migration is returned only if **every** statement has a certain inverse: creating a table, a view, an index, a sequence or a schema, or adding a column. It drops what the migration created, and is run with `database_sql_write`. A committed migration cannot be undone with `action_undo`.

### From the console

Open **Données** (Data), section « Migrations ».

1. Paste the migration's statements.
2. Click « Répéter puis annuler » (rehearse then roll back): the page shows the locks taken, the rehearsal's duration and the inverse migration if there is one.
3. Read the « Avant d'appliquer » (before applying) card, then click « Appliquer la migration » (apply the migration).

Only the maintainer rank applies; every application enters the audit log. The page shows « non mesuré » (not measured) for what the platform does not publish yet: the history of applied migrations, the rows touched and the waiting queries.

### Through an agent, the command line or the API

The `database_migrate` tool (rank `maintainer`, scope `db:write`) takes `sql` and, to stop after the rehearsal, `dryRun: true`. A person approves the call before it goes out; a service account does not run it.

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "sql": "alter table commandes add column livree_le timestamptz;",
  "dryRun": true,
  "idempotencyKey": "migration-livree-le-a-blanc-2026-09-14"
}
```

The response carries `applied`, `durationMs`, `locksAcquired` and `rollbackSql`. On the command line: `bastide database migrate --project-id <project> --sql <statements> --dry-run`. Over HTTP: `POST /v1/projects/{projectId}/database/migrations`.

## Schema drift

Every night, the platform replays the migrations in your repository's `supabase/migrations/` folder, in the order of their names, on a separate database built on the same base as yours. It then compares the two, object by object: schemas, extensions, tables, views, columns, constraints, indexes, policies, functions. Your repository's SQL runs there under a customer role, never as administrator.

Each drift has a nature and carries both definitions:

| Nature | What it says |
| --- | --- |
| `missing_in_database` | Declared in the repository, missing from the database. |
| `missing_in_repository` | Present in the database, missing from the repository: often a change made by hand. |
| `different` | Defined differently in the database. |

The state is `conforme` (matching), `ecart` (drift) or `non_mesure` (not measured). A comparison that could not be made (no connected repository, no migrations folder, a migration that does not replay) is "not measured", with its reason: an absence of drift never reads as a matching database.

A **new** drift emits the `database.schema.drifted` event to the project's endpoints (see [Webhooks](webhooks.md)). The same drift, the next night, does not emit again.

### Where to read it

- **Console**: **Données**, section « Migrations » (the drifts, with « Voir les définitions », show the definitions), and section « Sécurité » (security), under the posture.
- **Agent**: `backend_schema_drift_read` (rank `maintainer`, scope `db:read`).
- **Command line**: `bastide backend schema_drift read --project-id <project>`.
- **API**: `GET /v1/projects/{projectId}/backend/schema-drift`.

The response carries `comparedAt` (null until a comparison has run), `state`, `commitSha`, `declaredMigrations`, `comparedObjects`, `drifts` (at most one hundred, `truncated` beyond), `driftCount` and `notMeasured`.

## Common errors

- **`migration_destructive`**: your migration removes a column or a table. Use `database_sql_write`, which has it confirmed.
- **`migration_refusee_par_la_base`**: fix the named statement; nothing was applied.
- **`state: non_mesure`**: read `notMeasured`. Most often the repository has no `supabase/migrations/` folder, or a migration does not replay on an empty database.
- **A `missing_in_repository` drift after a test in the SQL editor**: carry the change into a migration in the repository, or undo it.

## See also

- [Database](database.md), [Backups](backups.md), [Previews](previews.md)
- Reference: [database.migrate](../reference/operations/database.migrate.md), [backend.schema_drift.read](../reference/operations/backend.schema_drift.read.md)
