I Upgraded MikroORM v6 to v7 and My DateTime Data Was Corrupted
I ran npm install @mikro-orm/core@^7.0.0 and thought I was done. My tests passed. My app started. Everything looked fine.
Then I checked my production data.
Datetime values that should have been 2024-03-15 14:30:00 were now 2024-03-15 22:30:00. Eight hours off—exactly my timezone offset from UTC. My data was silently corrupted.
This is the most dangerous breaking change in MikroORM v7, and it caught me completely off guard.
The Problem: forceUtcTimezone Defaults to True Now
In MikroORM v6, forceUtcTimezone defaulted to false. Your datetime values were stored as-is, preserving whatever timezone your application used.
In MikroORM v7, forceUtcTimezone defaults to true. This means datetime values are now converted to UTC before storage.
Here’s what happened to me:
Database: 2024-03-15 14:30:00 (stored as local time, no conversion)App reads: 2024-03-15 14:30:00 (correct)
After upgrade (v7 with forceUtcTimezone=true by default)Database: 2024-03-15 14:30:00 (still the old data, not converted)App reads: 2024-03-15 22:30:00 (converted from "UTC" to local time - WRONG!)The v7 ORM assumed my existing data was already in UTC. It wasn’t.
Step 1: Check Your Current Datetime Storage Before Upgrading
Before touching anything, I needed to understand how my data was stored:
import { EntityManager } from '@mikro-orm/core'import { MyEntity } from '../src/entities/MyEntity'
async function checkDatetimeStorage(em: EntityManager) { const sample = await em.findOne(MyEntity, {})
if (!sample?.createdAt) { console.log('No datetime data found') return }
const dbValue = sample.createdAt const now = new Date()
console.log('Database datetime:', dbValue.toISOString()) console.log('Current local time:', now.toString()) console.log('Current UTC time:', now.toISOString())
// Check if stored value matches what you expect const hoursDiff = (dbValue.getTime() - now.getTime()) / (1000 * 60 * 60) console.log('Hours difference from now:', hoursDiff)}I ran this script with my v6 ORM and confirmed: my data was stored in local timezone, not UTC.
Step 2: Pin forceUtcTimezone to Preserve Existing Behavior
The safest approach is to keep v6 behavior during the upgrade:
import { defineConfig } from '@mikro-orm/core'import { PostgreSqlDriver } from '@mikro-orm/postgresql'
export default defineConfig({ driver: PostgreSqlDriver, dbName: process.env.DB_NAME!, // CRITICAL: Match v6 behavior during upgrade forceUtcTimezone: false, // Explicitly set to false!})I added this setting before upgrading any packages. This ensured my app would continue reading and writing datetimes in local timezone, just like v6.
Step 3: Update Dependencies
Now the package updates. MikroORM v7 replaced Knex with Kysely as its query builder:
# Remove Knex if you had it as a direct dependencynpm uninstall knex
# Install Kysely (new peer dependency in v7)npm install kysely
# Update MikroORM packagesnpm install @mikro-orm/core@^7.0.0npm install @mikro-orm/postgresql@^7.0.0I missed the Kysely installation at first. My app crashed with:
Error: Cannot find module 'kysely'Don’t skip that step.
Step 4: Fix CLI Scripts for ESM
My package.json had this:
{ "scripts": { "orm": "mikro-orm-esm" }}The mikro-orm-esm script no longer exists in v7. ESM is now native. I changed it to:
{ "scripts": { "orm": "mikro-orm" }, "type": "module"}The unified mikro-orm CLI handles both CommonJS and ESM projects automatically. Just ensure your package.json has "type": "module" for ESM.
Step 5: Run Tests, Then Migrate Datetime Data
After confirming everything worked with forceUtcTimezone: false, I planned the actual data migration.
The approach I used:
import { EntityManager, raw } from '@mikro-orm/core'
async function migrateDatesToUtc(em: EntityManager) { // Get current timezone offset in hours const offsetHours = -new Date().getTimezoneOffset() / 60
// Update all datetime columns by subtracting the timezone offset // This converts local time to UTC
await em.getConnection().execute(` UPDATE my_table SET created_at = created_at - INTERVAL '${offsetHours} hours', updated_at = updated_at - INTERVAL '${offsetHours} hours' `)
console.log('Migration complete')}This is simplified. My actual migration involved:
- Backup the database first
- Identify all tables with datetime columns
- Run the offset correction on each
- Verify with sample queries
- Remove
forceUtcTimezone: falsefrom config (use the new defaulttrue)
The Benefits I Got from Upgrading
After the painful datetime migration, I appreciated the v7 improvements:
Zero runtime dependencies in core: The @mikro-orm/core package now has zero runtime dependencies. This means smaller bundles and the ability to use MikroORM entities in edge runtimes, browser environments, and serverless functions.
Kysely instead of Knex: Kysely provides better TypeScript type safety. My query types are now inferred correctly:
const users = await em.createQueryBuilder(User) .select(['id', 'email']) .where({ active: true }) .execute()
// users is typed as { id: number; email: string }[]// No more 'any' types!Native ESM support: No more separate CLI scripts or configuration complexity. The unified CLI just works.
Common Mistakes to Avoid
I made several mistakes during this upgrade. Learn from them:
Mistake 1: Not checking datetime storage format
I assumed my data was in UTC because “that’s best practice.” It wasn’t. Always verify.
Mistake 2: Forgetting to install Kysely
The error message was clear, but I wasted time debugging before realizing it was a missing peer dependency.
Mistake 3: Using old CLI script names
mikro-orm-esm doesn’t exist anymore. The error was cryptic until I checked the changelog.
Mistake 4: Not reading the upgrading guide
I skimmed it. I missed the forceUtcTimezone section. My data got corrupted. Read the full guide at https://mikro-orm.io/docs/upgrading-v6-to-v7.
A Safer Pre-Upgrade Checklist
Here’s the checklist I should have followed:
import { config } from '../mikro-orm.config'
interface CheckResult { storedAsUtc: boolean configForceUtc: boolean | undefined risk: 'low' | 'medium' | 'high' recommendation: string}
async function preUpgradeCheck(em: EntityManager): Promise<CheckResult> { // 1. Check if datetime data is stored in UTC const sample = await em.findOne(EntityWithDates, {}) const storedAsUtc = isStoredInUtc(sample?.createdAt)
// 2. Check current config const configForceUtc = config.get('forceUtcTimezone')
// 3. Assess risk const risk = assessRisk(storedAsUtc, configForceUtc)
return { storedAsUtc, configForceUtc, risk, recommendation: getRecommendation(storedAsUtc, configForceUtc) }}
function assessRisk(storedAsUtc: boolean, configForceUtc: boolean | undefined): 'low' | 'medium' | 'high' { // High risk: data is local, but v7 will default to UTC if (!storedAsUtc && configForceUtc === undefined) { return 'high' }
// Medium risk: explicit config needed to maintain behavior if (!storedAsUtc && configForceUtc === false) { return 'medium' }
return 'low'}What I’d Do Differently
If I could redo this upgrade, I would:
- Run the pre-upgrade check script first
- Set
forceUtcTimezone: falseexplicitly before any package updates - Read the entire upgrading guide, not just skim it
- Test datetime handling specifically in a staging environment
- Plan the UTC migration as a separate step, not rush it
The upgrade itself isn’t complicated. The datetime issue is the only dangerous part. Handle it carefully, and v7’s benefits are worth it.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments