Schema migrations
Generating, hand-reviewing, and shipping database migrations safely — autogenerate, the additive-then-destructive rule, and testing real upgrades.
When a PR touches the database schema — that is, app/storage/models.py — it needs a migration. Cremind uses Alembic, with the migration tree bundled inside the wheel at app/alembic/ so an installed copy can migrate itself without an alembic.ini on disk.
This page covers the workflow and the safety rules. Migrations are one of the few places where a mistake can corrupt user data, so the review bar is higher than for ordinary code.
The workflow
1. Generate the migration
cremind db revision --autogenerate -m "short_description"This diffs the live schema against the models and scaffolds a migration under app/alembic/versions/.
2. Review the autogenerate output by hand
Autogenerate is a draft, not an answer
Alembic's autogenerate misses or mis-handles several common changes. Always read the generated migration and fix it before committing.
Autogenerate is known to get these wrong:
- Column renames — it sees them as a drop plus an add, which loses the column's data. Rewrite as an actual rename.
server_defaultchanges — often not detected.- Check constraints — frequently missed.
- Enum additions — handled inconsistently across backends.
- Index renames — emitted as drop plus create.
3. Prefer additive shapes within one release
Within a single release, keep migrations additive: add columns, add tables, add indexes. Split destructive changes across two releases so a rollback always has somewhere safe to land:
- Release N — add the new shape and dual-write to both old and new.
- Release N+1 — drop the old shape, once you're confident nothing reads it.
This way an upgrade to N is reversible, and N+1 only removes a column that's already unused.
4. Backfill inside the migration
Populate new columns inside the migration, not in application code. A migration runs exactly once at a known point in the upgrade; application-side backfill races with serving traffic and leaves rows in mixed states.
5. Test the upgrade from a real old install
Test the upgrade against a real older install, not just a fresh database. A fresh DB exercises the baseline-create path; it does not exercise the data your migration actually has to transform. Stand up an install at the prior version, populate it, then upgrade.
The upgrade floor
MIN_SUPPORTED_UPGRADE_FROM in app/__version__.py is the oldest version this build can migrate from. It's currently 0.0.0, which accepts any install. Bump it only when a migration genuinely requires a minimum prior schema version — and when you do, the upgrader will refuse to proceed on installs older than that, so it's a real compatibility break, not a routine change.
How it runs at install time
On startup the runtime brings the schema to head automatically: a fresh database runs the baseline migration to create the tables; an existing database runs any pending migrations and is a fast no-op if it's already at head. You don't run an upgrade command by hand on a normal install — the migration ships in the wheel and applies itself.
Next
Versioning
The single source of truth in app/__version__.py, how it flows to PyPI, npm, and Docker, the PEP 440 vs SemVer forms, and the three release channels.
Docs conventions
How to add or edit a page in this documentation portal — the MDX file format, frontmatter, and the per-section meta.json sidebar.