---
title: "Migrate"
section: "Extensions"
framework: WebFluid
package: webfluid
version: "1.0.0b3"
stage: "beta"
released: "August 17, 2026"
canonical: "https://docs.webfluid.dev/latest/ext/migrate.md"
html: "https://docs.webfluid.dev/latest/ext/migrate"
audience: ai-agent
format: markdown
---

# Migrate


`Migrate` is a **CLI-only** extension: `wf migrate …` wraps Alembic, ships single- and
multi-database templates, picks the right one from your config and runs each command with the app's
environment. There is no runtime object and no `EXT_` switch — it is available whenever the package
is installed.


## Prerequisites

Migrate builds your application instead of running it, so it has hard structural requirements:

1. **`main.py` exposes `prepare_fluid()`.** The generated `env.py` calls it (falling back to a
   module-level `fluid`). A module-level app that is constructed at import time will be constructed
   with the migration environment, which is usually not what you want.
2. **Config lives in `fluid/config.py`**, not in `main.py`. `wf migrate init` reads the config
   without executing `main.py` at all — it calls `init_configs()` + `build_config()` directly. A
   `SQLALCHEMY_BINDS` defined anywhere else is invisible to template selection.
3. **`additives/` exists.** The generated `env.py` scans it. `wf create project` makes it;
   `mkdir additives` otherwise.

```python
# fluid/config.py
from webfluid.core.config import register_config


@register_config(10)
class Config:
    SQLALCHEMY_BINDS = {
        "test": "sqlite:///test.db"
    }
```

## The commands

```bash
wf migrate init <app>                       # enroll the alembic template for this app
wf migrate revision <app> -a -m "message"   # create a revision (-a = --autogenerate)
wf migrate upgrade <app>                    # upgrade to head
wf migrate downgrade <app> -r -1            # downgrade (-r/--revision, default -1)
```

Everything is per **app config**, not per project. Each app gets its own migration history:

```text
migrate/
├── alembic_app.ini
├── migrations_app/
│   ├── env.py
│   └── versions/
├── alembic_app2.ini
└── migrations_app2/
```

`init` refuses to overwrite an existing environment and tells you so.

## Which template

`wf migrate init` chooses:

| Condition                                              | Template    |
|--------------------------------------------------------|-------------|
| `SQLALCHEMY_BINDS` is non-empty in the resolved config | `multi_db`  |
| otherwise                                              | `single_db` |

The generated `alembic.ini` takes its log prefix from the framework's `ENV_PREFIX`, so migration
output lines up with the rest of the logs.

> **KNOWN BUG** — Template selection reads SQLALCHEMY_BINDS from the config at init time. Binds attached at runtime through Model.set_bind — which is how SECURITY_MODELS_DB_BIND, BABEL_DATABASE_BIND and most Additives do it — are not detected. If your binds only exist at runtime, declare them in SQLALCHEMY_BINDS before running init, or copy the multi_db template in by hand.

## What the migration environment sees

`wf migrate revision/upgrade/downgrade` shells out to `alembic` with a **manipulated environment**
built from `app_configs/<app>.ini`:

```python
env["EXT_SQLALCHEMY"] = "1"      # forced on
env["EXT_EVENTS"]     = "0"
env["EXT_CACHE"]      = "0"
env["EXT_MAIL"]       = "0"
env["EXT_JWT"]        = "0"
env["WF_THEMES"]      = "0"
env["WF_TAILWIND"]    = "0"
env["WF_PROCESSING"]  = "0"
```

Consequences worth planning around:

- **`*_FILE` keys are resolved here too** — the same parser as `wf run`, so a migration connects
  with the real secret behind a file-backed key.
- **`EXT_BABEL` and `EXT_SECURITY` are *not* forced off.** Their models therefore appear in
  autogenerated revisions when the app config enables them, which is what you want.
- **The surface is off.** No Tailwind compile, no frontend build, no processing context. Your
  models get built; your frontend does not.
- **`[dev]` is not applied.** `_manipulated_env` reads every section unconditionally, so a
  development database override in `[dev]` is *not* picked up by migrate. Use a separate app config
  for a development database.

The generated `env.py` imports `fluid.models` plus the models of every Additive that is **enabled
for this app**, and skips the rest. So a revision contains your tables and the tables of the
Additives you actually switched on — not everything sitting in `additives/`.

## SQLite: batch mode

Both templates pass `render_as_batch` when the dialect is SQLite, in `run_migrations_offline`
(`url.startswith("sqlite")`) and in `do_run_migrations` (`connection.dialect.name == "sqlite"`).
SQLite has no real `ALTER TABLE`, so without it a column rename, a type change or an added
constraint fails on the database most projects develop against. Batch mode rebuilds the table and
copies the rows.

> **WARNING** — env.py is generated once by wf migrate init and is yours from then on. A migration environment created before 1.0.0b3 does not have this. Add render_as_batch=True to both context.configure(...) calls in your env.py, or generate a fresh environment elsewhere and copy the lines across.

## Full flow

```bash
# 1. Make sure the app config enables the extension
#    [extensions]
#    EXT_SQLALCHEMY = 1

# 2. Enroll the environment (once per app)
wf migrate init app

# 3. Every time your models change
wf migrate revision app -a -m "add entries table"

# 4. Apply
wf migrate upgrade app

# 5. Run
wf run app
```

Revision messages are prefixed automatically with `[<app>] [<UTC timestamp>] `, so `-m` only carries
your text.

### Multiple databases

```bash
cp app_configs/app.ini app_configs/app2.ini
# point DATABASE_URI at another database, and make sure
# SQLALCHEMY_BINDS is defined in fluid/config.py

wf migrate init app2        # -> multi_db template
wf migrate revision app2 -a
wf migrate upgrade app2
```

## Rules

> **RULE** — Always review an autogenerated revision before applying it. Alembic's autogenerate does not detect column renames (it emits drop + add, losing data), server-default changes on some backends, or type changes it considers equivalent.

> **RULE** — Do not mix create_all with migrations on the same database. A startup hook that calls create_all will create tables Alembic then wants to create again. Pick one; for anything you deploy, pick migrations.

> **RULE** — Re-run the revision + upgrade cycle after enabling EXT_BABEL or EXT_SECURITY. Both bring models — I18nKey / I18nMessage for Babel, and the whole user/role/permission/2FA/token set for Security — that must exist before the app can serve a request.

> **WARNING** — Upgrading a beta 1 translation database: I18nKey.key was unique across the whole table and is now unique per (key, domain). That needs a migration to replace the index, and any messages a second domain wrote against the first domain's key belong to that first domain and have to be re-imported under their own.

## Running Alembic directly

Everything Migrate does is a subprocess call, so anything Alembic offers is reachable:

```bash
alembic -c migrate/alembic_app.ini history
alembic -c migrate/alembic_app.ini current
```

You lose the manipulated environment when you do that, so set `EXT_SQLALCHEMY=1` and your
`DATABASE_URI` yourself, or stick to the `wf migrate` wrappers for anything that connects.

## Next

- [`ext/sqlalchemy.md`](/latest/ext/sqlalchemy.md) — the models being migrated.
- [`config/app-config.md`](/latest/config/app-config.md) — the file migrate reads.
- [`cli/create.md`](/latest/cli/create.md) — the factory layout migrate expects.


---

## Navigation


- [Overview](/latest/.md) — what WebFluid is, how to navigate these docs, release state and known bugs
- [Getting Started](/latest/get-started.md) — install, minimal app, the layout the framework expects

**Configuration**
- [App configs](/latest/config/app-config.md) — `app_configs/<app>.ini`, switches, `*_FILE` secrets, `[dev]`
- [Config classes](/latest/config/config-class.md) — `register_config`, `DefaultConfig`, every framework key

**Extensions**
- [Introduction](/latest/ext/base.md) — `FluidExtension`, entry points, `expand_fluid`
- [Scheduling](/latest/ext/scheduling.md) — APScheduler `AsyncIOScheduler`
- [SQLAlchemy](/latest/ext/sqlalchemy.md) — `Model`, executors, binds, sessions
- [Migrate](/latest/ext/migrate.md) — Alembic wrapper, `prepare_fluid`, single/multi-db
- [Mail](/latest/ext/mailman.md) — SMTP, sync/async, batching, attachments
- [Babel](/latest/ext/babel.md) — gettext, domains, locale selection, formatters
- [Security](/latest/ext/security.md) — users, gates, CSRF, tokens, OAuth, bearer grants
- [Events](/latest/ext/events.md) — signals, events, queries, browser socket
- [Cache](/latest/ext/cache.md) — redis / legacy backends
- [JWTManager](/latest/ext/jwt.md) — encode/decode, rotation, revocation

**Frontend (surface)**
- [Introduction](/latest/surface/tooling.md) — feature switches, `fluid_base.html`, Tailwind, themes
- [Integration](/latest/surface/frontend.md) — `APP_FRONTEND`, htmx, Vite workspaces
- [Template resolution](/latest/surface/jinja.md) — loader order, namespaces, overriding

**Additives**
- [Introduction](/latest/additives/intro.md) — manifest, routers, enabling
- [Base Additives](/latest/additives/base.md) — `type: base`, `import_base`, extension semantics
- [Interaction between](/latest/additives/contract.md) — events/queries as contracts, requirements, packaging

**Framework utility**
- [Lifecycle](/latest/utils/lifecycle.md) — `mix()`, startup/shutdown hooks, graceful stop
- [Runtime](/latest/utils/runtime.md) — `FluidContext`, request hooks, themes, proxy, rate limits
- [Logging](/latest/utils/logging.md) — the log factory, levels, files, additive attribution

**CLI**
- [Introduction](/latest/cli/wf.md) — command tree, extension CLIs
- [Create](/latest/cli/create.md) — `wf create project/app/additive`
- [Run](/latest/cli/run.md) — `wf run`, flags, debug and interactive mode
- [Ocean](/latest/cli/ocean.md) — search, install, publish

**Reference**
- [Overview](/latest/ref.md) — package layout, stability contract
- [Core](/latest/ref/core.md) — `Fluid`, config, context, `core.ext`, constants
- [Extensions](/latest/ref/extensions.md) — the battery API surface
- [Surface](/latest/ref/surface.md) — `Frontend`, Node and Tailwind tooling
- [Additives](/latest/ref/additives.md) — `Additive`, `Router`, `Manifest`, registry helpers
- [Utils](/latest/ref/utils.md) — helpers, logging, exceptions