Safe Realm Database Schema Migrations: A Step-by-Step Guide for Mobile Apps
Step-by-step guide to performing safe Realm Database schema migrations using versioned schemas and migration blocks, with prerequisites, validation checks, and rollback strategies for mobile apps.
03 Sept 2026, 08:44 UTC

Desired Outcome
Apply additive schema changes — such as new properties or new object types — to an existing Realm Database without losing user data. Realm handles this through a migration block that runs automatically when the schemaVersion in Realm.Configuration is incremented. The block receives a Migration object with access to both the old and new Realm instances, allowing you to transform objects on first open after an app update.
Prerequisites
- Realm SDK 10.x or later integrated via Swift Package Manager, CocoaPods, or Carthage.
- Current
schemaVersioninteger stored in yourRealm.Configuration(default is 0). - Backup strategy for production data: use
Realm.copyRealm(configuration:)to create a pre-migration snapshot, or rely on server-side backup for synced realms. - Test devices or simulators with representative datasets covering each prior schema version.
- Understanding of property attribute changes: making a property required, adding an index, or changing optionality.
Procedure
1. Increment the schema version
Update the configuration where you initialize Realm, typically in AppDelegate or a dedicated Realm manager:
let config = Realm.Configuration(
schemaVersion: 1, // was 0
migrationBlock: { migration, oldSchemaVersion in
// migration logic here
}
)
Realm.Configuration.defaultConfiguration = configEach release that changes the schema must bump this integer by exactly 1.
2. Define the migration block
The block runs on the same thread that opens the Realm. Keep it fast — avoid network calls or heavy computation.
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
// Example: add required 'createdAt' property to Task objects
migration.enumerateObjects(ofType: Task.className()) { oldObj, newObj in
let defaultDate = Date()
newObj?["createdAt"] = defaultDate
}
}
}Key points:
- Use
oldSchemaVersionto guard incremental steps; the block runs once per version bump. - Access properties via string keys — Realm Swift 10+ stores property names in lowercase with underscores (e.g., Swift
createdAtbecomescreated_atin the schema). - For renames, create a new object and copy values:
newObj?["newName"] = oldObj?["oldName"]. - Deletions require no action; Realm ignores removed properties.
3. Handle property type changes
Changing a property type (e.g., String to Int) is not automatic. Convert manually in the block:
if oldSchemaVersion < 2 {
migration.enumerateObjects(ofType: User.className()) { oldObj, newObj in
if let str = oldObj?["ageString"] as? String,
let intVal = Int(str) {
newObj?["age"] = intVal
} else {
newObj?["age"] = 0 // fallback
}
}
}4. Test the migration
- Create a test Realm file with
schemaVersion = 0and sample data (use a unit test or a debug build). - Increment to
schemaVersion = 1with the migration block above. - Open the Realm — the block executes and transforms objects.
- Verify the new property exists and holds the default value.
Expected Checks
- Migration block executes exactly once per version increment — log entry at start/end of block.
- All existing objects have valid values for new required properties (no
nilwhere non-optional). - Object counts before and after migration match; run
realm.objects(Task.self).countin tests. - Indexes and primary keys remain functional — query by primary key and indexed properties.
- Test both upgrade paths: clean install (no migration) and sequential upgrades (v0 → v1 → v2).
Recovery Options
If the migration block throws (e.g., MigrationException), Realm aborts the open. Catch and log:
do {
let realm = try Realm(configuration: config)
} catch let err as MigrationException {
// Option 1: delete and start fresh (dev only)
try? Realm.deleteFiles(for: config)
// Option 2: prompt user to reinstall or contact support
// Option 3: fall back to a read-only backup copy
}For production:
- Create a pre-migration snapshot with
Realm.copyRealm(configuration:)before opening. - Implement server-side backup for synced realms (Flexible Sync or Partition Sync).
- Automate CI tests that run the full migration chain from v0 to current on every PR.
Limitations and Caveats
- Migration blocks run on the calling thread — do not block the main thread with complex transforms.
- Removing a property does not reclaim storage until
Realm.compactRealm(configuration:)is called; plan for bloat if you frequently drop columns. - Synced realms require additive-only, backward-compatible changes; the server schema must accept the new properties.
- Property names in the migration block use the stored schema naming (snake_case), not Swift camelCase.
Verification Steps
- Open the migrated
.realmfile in Realm Studio — confirm schema version, property presence, and data integrity. - Run automated migration tests in CI for each version step, simulating fresh install and sequential upgrades.
- Monitor migration block execution count via logging to ensure it runs only once per version increment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.