Migration Guide
How database migrations work in Morgenruf, how to upgrade between versions, and how to roll back when something goes wrong.
How Migrations Work
Morgenruf uses a custom raw-SQL migration runner β not Alembic or Flask-Migrate. Migrations are plain .sql files stored in app/migrations/ and are executed in alphabetical (filename) order.
The runner tracks which migrations have been applied in a schema_migrations table that is created automatically on the first run:
CREATE TABLE IF NOT EXISTS schema_migrations (
filename TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ DEFAULT NOW()
);
sql
On each startup (or manual run), the runner:
- Creates the
schema_migrationstable if it doesn't exist. - Collects all
*.sqlfiles inapp/migrations/, sorted by filename. - Skips files that already appear in
schema_migrations. - Executes each new file inside a transaction β if the SQL fails, the transaction is rolled back and startup aborts.
- Records the filename in
schema_migrationson success.
Each migration file is idempotent at the runner level: it is applied exactly once. The SQL inside may or may not use IF NOT EXISTS guards.
Migration Files
All 15 migration files shipped with Morgenruf v1.0.0, in application order:
| File | Introduced in | What it adds |
|---|---|---|
001_initial.sql | v0.1.0 | Core tables: installations, workspace_config, members, standups |
002_standup_config.sql | v0.2.0 | Extended workspace configuration columns |
003_webhooks.sql | v0.2.0 | webhooks table for outbound event delivery |
004_edit_window.sql | v0.2.0 | edit_window_minutes column on workspace_config |
005_mood.sql | v0.3.0 | mood column on standups |
006_standup_schedules.sql | v0.4.0 | standup_schedules table for multiple schedules per workspace |
007_autolink.sql | v0.4.0 | autolink_patterns table for Jira / GitHub / Linear auto-linking |
008_rbac.sql | v0.4.0 | role column on members for admin/member RBAC |
009_ai_summary.sql | v0.4.0 | ai_summaries table for AI-generated standup digests |
010_kudos.sql | v0.4.0 | kudos table for peer recognition |
011_workflow_rules.sql | v0.4.0 | workflow_rules table for automation triggers and actions |
012_platforms.sql | v0.4.0 | platform column on installations for Google Chat support |
013_feed_token.sql | v0.4.0 | feed_token column on workspace_config for public standup feeds |
014_manager_email.sql | v0.4.0 | manager_email column on workspace_config for daily digest emails |
015_mcp_api_keys.sql | v1.0.0 | mcp_api_keys table for MCP Bearer token authentication |
Running Migrations
Automatic on startup
Migrations run automatically every time the application starts. No manual steps are required for normal deployments β just pull the new image and restart.
Manual run
To run migrations without starting the full application (useful for CI or pre-flight checks), pass the migrate command to the container:
docker run --rm \
-e DATABASE_URL=postgresql://user:pass@host:5432/morgenruf \
ghcr.io/morgenruf/morgenruf:latest migrate
bash
With Docker Compose, you can run a one-off migration container before starting the app:
docker compose run --rm app migrate
bash
Kubernetes init container
For Kubernetes deployments it is recommended to run migrations as an init container so they complete before the main pod starts serving traffic. Add this to your values.yaml:
initContainers:
- name: migrate
image: ghcr.io/morgenruf/morgenruf:latest
command: ["python", "-m", "src.migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: morgenruf-secrets
key: DATABASE_URL
yaml
Checking applied migrations
To see which migrations have been applied, query the schema_migrations table directly:
SELECT filename, applied_at
FROM schema_migrations
ORDER BY applied_at;
sql
Upgrading Between Versions
Morgenruf follows Semantic Versioning. The general upgrade process is:
- Back up your database before any upgrade (see Backup & Restore below).
- Pull the new Docker image tag.
- Restart the container β migrations run automatically.
- Verify the app is healthy with
GET /healthz.
# Pull the target version
docker pull ghcr.io/morgenruf/morgenruf:1.0.0
# Restart (docker-compose)
docker compose up -d
# Verify
curl https://yourapp.example.com/healthz
bash
For Kubernetes / Helm:
helm upgrade morgenruf ./helm/morgenruf \
--set image.tag=1.0.0 \
--reuse-values
bash
Rollback Procedures
The migration runner does not provide automatic down-migrations. SQL schema changes (adding tables, adding columns) are generally safe and backwards-compatible, so rolling back the application image is usually sufficient.
To roll back to a previous version:
- Roll back the application image to the previous tag.
- If the new migrations added columns that the old code doesn't know about, that is fine β the old code ignores unknown columns.
- If a migration was destructive (dropped a column or table), restore from backup (see below).
# Roll back image (docker-compose)
docker compose stop app
docker compose up -d --no-deps app # after editing image tag in docker-compose.yml
# Or with Helm
helm rollback morgenruf
bash
To prevent a migration from re-running after a rollback, do not delete its row from schema_migrations. Leave it in place so the runner continues to skip it.
Manually reverting a migration
If you need to undo a migration (e.g. drop a table added by mistake), write the inverse SQL manually, run it, and then delete the migration's row from schema_migrations:
-- Example: undo 015_mcp_api_keys.sql
DROP TABLE IF EXISTS mcp_api_keys;
DELETE FROM schema_migrations WHERE filename = '015_mcp_api_keys.sql';
sql
Backup & Restore
Always back up before upgrading. Use pg_dump for a consistent snapshot:
# Dump
pg_dump "$DATABASE_URL" --format=custom --file=morgenruf_backup_$(date +%Y%m%d).dump
# Restore
pg_restore --clean --dbname "$DATABASE_URL" morgenruf_backup_20260405.dump
bash
In Kubernetes, you can run a one-off pod:
kubectl run pg-dump --rm -it --restart=Never \
--image=postgres:16 \
--env="PGPASSWORD=yourpassword" \
-- pg_dump -h db-host -U user morgenruf \
--format=custom > morgenruf_backup.dump
bash
Version-Specific Migration Notes
v1.0.0 (2026-04-05)
New migration: 015_mcp_api_keys.sql β adds the mcp_api_keys table. No data changes; fully additive.
If you were using the MCP server from v0.4.0 without API keys (e.g. in a private deployment with no auth), you must generate an API key in Dashboard β Integrations β MCP after upgrading. Unauthenticated MCP requests now return 401.
v0.4.0 (2026-04-05)
New migrations: 006 through 014 β ten additive migrations adding schedules, RBAC, AI summaries, kudos, workflow rules, Google Chat platform support, feed tokens, manager email, and autolink patterns.
Breaking change β RBAC: Migration 008_rbac.sql adds a role column to the members table. The dashboard now enforces admin-only access for configuration changes. The user who originally installed the app is automatically granted the admin role during the OAuth callback. If you run into permission errors after upgrading, run:
UPDATE members SET role = 'admin'
WHERE user_id = '<your_slack_user_id>'
AND team_id = '<your_team_id>';
sql
Multiple schedules: Migration 006_standup_schedules.sql introduces a new standup_schedules table. Existing workspace configurations in workspace_config continue to work; the new table is used by the multi-schedule UI introduced in v0.4.0.
v0.3.0 (2026-03-20)
New migration: 005_mood.sql β adds a mood column to standups. Existing standup rows will have mood = NULL, which is treated as "not answered" by the analytics dashboard.
v0.2.0 (2026-03-01)
New migrations: 002_standup_config.sql, 003_webhooks.sql, 004_edit_window.sql β additive changes only.
The web dashboard is introduced in this release. After upgrading, set the SESSION_SECRET environment variable if you haven't already β sessions will not persist without it. See Configuration β Environment Variables.
v0.1.0 (2026-02-15)
Initial release. 001_initial.sql creates all core tables from scratch. Requires PostgreSQL 13 or newer.
Writing Custom Migrations
If you have forked Morgenruf or need to add custom schema changes, drop a new .sql file in app/migrations/. Name it with the next available three-digit prefix (e.g. 016_my_change.sql) so it sorts after all existing migrations.
Guidelines:
- Keep each file focused on a single logical change.
- Use
IF NOT EXISTS/IF EXISTSguards where possible to make the SQL safe to inspect manually. - Never modify existing migration files β add a new one instead.
- Test your migration against a copy of production data before deploying.
-- 016_my_feature.sql
ALTER TABLE members ADD COLUMN IF NOT EXISTS department TEXT;
CREATE INDEX IF NOT EXISTS members_dept_idx ON members(team_id, department);
sql