Key rotation is a compatibility migration, not replacing an environment variable and hoping old data still opens. Readers must understand the old and new key identifiers before any writer starts using the new one.
This tutorial builds on the authenticated field envelope. The download includes that module and a standalone rotation transform tested on Node.js 20.11.1. The database compare-and-swap examples target MySQL 8.4; no live database migration was performed.
Separate format version from key identifier
An envelope's format version describes how to parse and authenticate it. Its key identifier selects the decryption key. Changing a key should not require inventing a new envelope format.
Keep keys immutable under their identifiers. Replacing the bytes associated with key-2026-a would make old records unreadable while still looking structurally valid. Add a new identifier instead.
The module binds the key identifier, record ID and field name into authenticated context. Migration must therefore decrypt with the old context and encrypt a new envelope, not merely edit the identifier.
rotate-encryption-keys-without-losing-records/
├── README.md
├── fields.mjs
├── rotate.mjs
└── test.mjsRoll out readers before writers
First distribute the new key to authorized readers while retaining the old keys. Verify every application instance can decrypt test records under both generations. Only then switch the active write key.
During a rolling deployment, lagging instances may still write using the old key. Keep observing the distribution of key identifiers until all writers are upgraded. A migration that reaches zero old rows once is not proof that an old worker cannot create another tomorrow.
Limit access to historical keys, document their purpose and separate operational access from archival recovery where possible. Key lifecycle and recovery planning are explicit responsibilities, not properties supplied by AES. OWASP key-management guidance.
Migrate in bounded batches
The standalone transform decrypts each record, re-encrypts with the target key when necessary and returns the same envelope when it is already current. Its idempotence makes restarting a job easier.
Read a small batch ordered by a stable primary key. Include a revision number with each row. After computing the new envelope, update only if nobody changed the record in the meantime:
UPDATE private_notes
SET encrypted_note = ?,
key_id = ?,
revision = revision + 1
WHERE id = ?
AND revision = ?
AND key_id = ?;Store key_id consistently with the envelope and validate that agreement on reads. One affected row means the migration won the comparison. Zero means the record changed or was already migrated; reload it instead of overwriting the user's newer content.
Do not keep a large transaction open while decrypting thousands of records. Track progress after committed batches, bound retries, and send malformed or unreadable records to a restricted review queue. Never replace a failed decryption with an empty value.
Keep rollback realistic
Before migration, take a protected backup and prove you can restore it with the corresponding historical keys. Keep an application version capable of reading all envelopes that may exist during rollback.
Rolling back the application to a build that knows only the old key will break records already written under the new key. A safe application rollback therefore retains dual-key reading, even if the migration pauses.
If rotation responds to a suspected compromise, moving future writes to a fresh key does not make previously exposed plaintext secret again. Record the incident separately from the maintenance migration and reassess credentials and access paths.
Test retirement, not just re-encryption
Run node --test test.mjs. The suite migrates one hundred synthetic mixed-key records, checks plaintext preservation, repeats the migration without rewriting current envelopes and verifies that an old backup record fails without its old key but succeeds when that key is restored.
Those tests exercise the transform only. In a staging database, add concurrent user edits, restart midway through a batch and verify compare-and-swap conflicts do not lose updates. Then restore a historical backup into an isolated environment.
Retire old keys only after live data, replicas, exports and retained backups have been accounted for. A long backup-retention policy may require old keys to remain available in restricted recovery storage long after active records migrate.
If some rows will not decrypt, inspect key identifiers and authenticated record context before attempting bulk repairs. For help designing the migration and recovery workflow, explore Next.js application development.
