morgenruf.dev

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:

  1. Creates the schema_migrations table if it doesn't exist.
  2. Collects all *.sql files in app/migrations/, sorted by filename.
  3. Skips files that already appear in schema_migrations.
  4. Executes each new file inside a transaction β€” if the SQL fails, the transaction is rolled back and startup aborts.
  5. Records the filename in schema_migrations on 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:

FileIntroduced inWhat it adds
001_initial.sqlv0.1.0Core tables: installations, workspace_config, members, standups
002_standup_config.sqlv0.2.0Extended workspace configuration columns
003_webhooks.sqlv0.2.0webhooks table for outbound event delivery
004_edit_window.sqlv0.2.0edit_window_minutes column on workspace_config
005_mood.sqlv0.3.0mood column on standups
006_standup_schedules.sqlv0.4.0standup_schedules table for multiple schedules per workspace
007_autolink.sqlv0.4.0autolink_patterns table for Jira / GitHub / Linear auto-linking
008_rbac.sqlv0.4.0role column on members for admin/member RBAC
009_ai_summary.sqlv0.4.0ai_summaries table for AI-generated standup digests
010_kudos.sqlv0.4.0kudos table for peer recognition
011_workflow_rules.sqlv0.4.0workflow_rules table for automation triggers and actions
012_platforms.sqlv0.4.0platform column on installations for Google Chat support
013_feed_token.sqlv0.4.0feed_token column on workspace_config for public standup feeds
014_manager_email.sqlv0.4.0manager_email column on workspace_config for daily digest emails
015_mcp_api_keys.sqlv1.0.0mcp_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:

  1. Back up your database before any upgrade (see Backup & Restore below).
  2. Pull the new Docker image tag.
  3. Restart the container β€” migrations run automatically.
  4. 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:

  1. Roll back the application image to the previous tag.
  2. If the new migrations added columns that the old code doesn't know about, that is fine β€” the old code ignores unknown columns.
  3. 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 EXISTS guards 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