Troubleshooting: Database and Migrations
Migrations are forward-only and applied by an explicit operator step
(make migrate), never automatically on startup. Most problems here are the
migration runner refusing to proceed, which is the intended behaviour: it stops
rather than applying a schema change it cannot account for.
Start from Troubleshooting if you are not sure this is the right page.
The migration role is missing
MERLON_MIGRATION_DATABASE_URL is required in production
Or, outside production:
using MERLON_DATABASE_URL as migration role; production must use a separate role
Schema changes and serving traffic use different roles on purpose. The serving
role has no DDL rights and cannot modify audit_logs; if migrations ran as the
serving role, the serving role would need exactly the privileges the audit
controls exist to deny it.
Set both, pointing at the same database with different roles:
export MERLON_DATABASE_URL='postgres://merlon_app:...@host:5432/merlon'
export MERLON_MIGRATION_DATABASE_URL='postgres://merlon_migrate:...@host:5432/merlon'
make migrate
Outside production the warning is not fatal, so a development shortcut will not block you — but it means development is not exercising the privilege split that production enforces.
A migration checksum does not match
migration 021 checksum mismatch: ledger=<sha256> file=<sha256>
The runner records a SHA-256 of every migration it applies. This says the file on disk now differs from the file that was applied to this database.
That is a rollout-stopping condition, and the runner is right to stop: the database's actual schema no longer corresponds to the migration history in the repository, so no later migration can be reasoned about.
Usual causes, in order of likelihood:
- An already-applied migration was edited. Applied migrations are immutable. Revert the file to its applied content and put the change in a new migration.
- The deployment was pointed at a database from a different branch or
environment. Confirm which database
MERLON_MIGRATION_DATABASE_URLactually resolves to. - Line endings changed. The checksum covers bytes. A file rewritten with CRLF hashes differently even though it is the same SQL.
Do not delete rows from schema_migrations to move past this. That does not
reconcile the schema, it only removes the evidence that it diverged.
An existing database has no ledger
MERLON_MIGRATION_BASELINE "0xx_name.sql" does not match a migration filename
Or, on a database that predates the ledger, the runner tries to apply migrations that are already present.
The runner never infers a baseline from table contents — guessing which migrations "look applied" is how a schema gets silently half-applied. You state it:
MERLON_MIGRATION_BASELINE=017_retention.sql make migrate
Every migration up to and including that filename is recorded as applied
without being run; everything after it is applied normally. The value must be
an exact filename from migrations/ — run ls migrations/ and copy one,
rather than reconstructing a name from what a table is called.
Take a backup before doing this. A wrong baseline skips real migrations, and the ledger will then claim they ran.
Restored data cannot be decrypted
Direct-PII customer attributes are encrypted at rest with
MERLON_ENCRYPTION_KEY_RING. The keys are not in the database.
A database backup without the corresponding key ring is permanently unreadable. There is no recovery path — not a support path, not a vendor path. The data is gone.
If a restored database returns errors or unreadable values on customer attributes, the key ring does not contain the key that encrypted them. Restore the key ring from the point in time matching the database backup.
Key rotation re-encrypts in batches without downtime, which means a backup can predate a rotation. Retain retired keys for at least as long as you retain backups that were written under them. See Backup and Restore.
Verifying after any of the above
# Applying twice must be a no-op — the second run reports nothing to apply.
make migrate && make migrate
# The append-only audit log must still verify.
cd api && go run ./cmd/merlon-audit verify
# Readiness must be clean.
curl -s http://localhost:8080/healthz/ready