Migration Endpoint Security (MIGRATION_SECRET)
Migration Endpoint Security
The /api/migrate endpoint applies pending database schema migrations against the production database. From v1.0.318 onwards, this endpoint is protected exclusively by a dedicated MIGRATION_SECRET environment variable. The previous fallback to NEXTAUTH_SECRET has been removed.
Why a Dedicated Secret?
Previously, /api/migrate fell back to NEXTAUTH_SECRET when MIGRATION_SECRET was not set. This meant:
- A single leaked credential granted access to both session signing and database schema changes.
- There was no clear signal to operators that a separate secret was expected.
Separating the secrets limits blast radius: a leaked NEXTAUTH_SECRET can no longer be used to trigger migrations.
Setup
1. Generate a dedicated secret
openssl rand -hex 32
This value must not be the same as your NEXTAUTH_SECRET.
2. Set the environment variable
Add MIGRATION_SECRET to your deployment platform (Vercel, Railway, etc.) and to your local .env file:
# .env
MIGRATION_SECRET=<your-generated-secret> # Must differ from NEXTAUTH_SECRET
3. Verify at startup
When the server starts, instrumentation.ts validates the secret automatically. In production, the server will refuse to start if either of the following conditions is true:
MIGRATION_SECRETis absent or empty.MIGRATION_SECRETequalsNEXTAUTH_SECRET.
In development, the same conditions produce a console.warn warning instead of a fatal error, so local environments without all secrets configured are not blocked.
Calling the Endpoint
curl -X POST https://your-app.vercel.app/api/migrate \
-H "Authorization: Bearer $MIGRATION_SECRET"
The endpoint is also called automatically from instrumentation.ts on every server cold-start, so manual calls are only needed when you want to apply migrations without redeploying.
Response Codes
| Status | Meaning |
|---|---|
200 OK | Migrations applied successfully. |
401 Unauthorized | Bearer token does not match MIGRATION_SECRET. |
503 Service Unavailable | MIGRATION_SECRET is not configured on the server. |
Boot-time Validation Behaviour
The validateMigrationSecret() function in src/instrumentation.ts runs at module load, before any request is handled.
| Environment | MIGRATION_SECRET absent | MIGRATION_SECRET === NEXTAUTH_SECRET |
|---|---|---|
| Production | Fatal — server refuses to start | Fatal — server refuses to start |
| Development / Test | console.warn | console.warn |
Build (NEXT_PHASE=phase-production-build) | Skipped | Skipped |
Migration from Earlier Versions
If you are upgrading from a version prior to v1.0.318:
- Generate a new secret with
openssl rand -hex 32. - Set it as
MIGRATION_SECRETin all production environments before deploying. - Ensure the value differs from
NEXTAUTH_SECRET. - Update any CI/CD scripts or runbooks that called
/api/migratewith theNEXTAUTH_SECRETbearer token — they must now useMIGRATION_SECRET.
Note: For additional hardening, consider removing the
/api/migrateHTTP endpoint entirely and relying on CI/CD pipeline migrations (e.g.drizzle-kit pushordrizzle-kit migraterun from a trusted build step). This removes the attack surface entirely.