Skip to Content
AdvancedMigration Guide

Handle Flutter InstantDB package updates, data migrations, and schema changes with confidence using proven migration strategies and tools.

Package Updates

Semantic Versioning

Flutter InstantDB follows semantic versioning (semver):

  • Patch releases (0.1.1 → 0.1.2): Bug fixes, no breaking changes
  • Minor releases (0.1.0 → 0.2.0): New features, backward compatible
  • Major releases (0.1.0 → 1.0.0): Breaking changes, requires migration

Safe Update Process

Follow this process for safe updates:

// 1. Check current version void checkCurrentVersion() { // Add to pubspec.yaml temporarily to see current version print('Current InstantDB version: check pubspec.yaml'); } // 2. Read changelog before updating // Always check CHANGELOG.md on GitHub or pub.dev // 3. Update in development first // Test thoroughly in development before production // 4. Create backup of critical data if needed Future<void> backupCriticalData(InstantDB db) async { final criticalEntities = ['users', 'payments', 'orders']; final backup = <String, List<Map<String, dynamic>>>{}; for (final entityType in criticalEntities) { final result = await db.queryOnce({entityType: {}}); backup[entityType] = (result.data?[entityType] as List? ?? []) .cast<Map<String, dynamic>>(); } // Save backup to file or external storage await _saveBackup(backup); } Future<void> _saveBackup(Map<String, dynamic> backup) async { // Implementation depends on your backup strategy // Could be local file, cloud storage, etc. }

Breaking Changes Migration

Handle breaking changes systematically:

class MigrationManager { final String fromVersion; final String toVersion; MigrationManager({ required this.fromVersion, required this.toVersion, }); Future<void> migrate(InstantDB db) async { print('Migrating from $fromVersion to $toVersion'); // Apply migrations based on version ranges if (_shouldApply('0.1.0', '0.2.0')) { await _migrateFrom01To02(db); } if (_shouldApply('0.2.0', '1.0.0')) { await _migrateFrom02To10(db); } // Update stored version await _setMigrationVersion(toVersion); } bool _shouldApply(String minVersion, String maxVersion) { // Implement version comparison logic return _isVersionInRange(fromVersion, minVersion, maxVersion); } bool _isVersionInRange(String version, String min, String max) { // Simple version comparison - you might want a more robust implementation final versionParts = version.split('.').map(int.parse).toList(); final minParts = min.split('.').map(int.parse).toList(); final maxParts = max.split('.').map(int.parse).toList(); return _compareVersions(versionParts, minParts) >= 0 && _compareVersions(versionParts, maxParts) < 0; } int _compareVersions(List<int> v1, List<int> v2) { for (int i = 0; i < 3; i++) { final diff = v1[i] - v2[i]; if (diff != 0) return diff; } return 0; } // Example migration from 0.1.0 to 0.2.0 Future<void> _migrateFrom01To02(InstantDB db) async { print('Applying 0.1.0 → 0.2.0 migration'); // Example: API change in transaction methods // Old: db.transactChunk() → New: db.transact() // This would be handled automatically by the package, // but you might need to update your code // Example: Schema changes await _updateUserSchema(db); } Future<void> _migrateFrom02To10(InstantDB db) async { print('Applying 0.2.0 → 1.0.0 migration'); // Major version might have significant changes await _updateAuthSystem(db); await _migratePresenceSystem(db); } Future<void> _updateUserSchema(InstantDB db) async { // Example: Add new required fields to existing users final users = await db.queryOnce({'users': {}}); final userList = (users.data?['users'] as List? ?? []) .cast<Map<String, dynamic>>(); final operations = <Operation>[]; for (final user in userList) { if (!user.containsKey('profileVersion')) { operations.add( db.update(user['id'], { 'profileVersion': 2, 'preferences': { 'notifications': true, 'theme': 'auto', }, }), ); } } if (operations.isNotEmpty) { await db.transact(operations); print('Updated ${operations.length} user profiles'); } } Future<void> _updateAuthSystem(InstantDB db) async { // Example: Migration for auth system changes print('Updating auth system...'); // Handle auth token format changes, session migration, etc. final currentUser = db.auth.currentUser.value; if (currentUser != null) { // Refresh user data with new format await db.auth.refreshUser(); } } Future<void> _migratePresenceSystem(InstantDB db) async { // Example: Presence API changes print('Migrating presence system...'); // Clean up old presence data that might be incompatible // This is conceptual - actual implementation depends on changes } Future<void> _setMigrationVersion(String version) async { // Store migration version in local storage final prefs = await SharedPreferences.getInstance(); await prefs.setString('instantdb_migration_version', version); } }

Automated Migration Runner

Create an automated migration system:

class AutoMigrationRunner { static const String _versionKey = 'instantdb_migration_version'; static const String _currentVersion = '1.0.0'; // Your current package version static Future<void> runMigrationsIfNeeded(InstantDB db) async { final prefs = await SharedPreferences.getInstance(); final storedVersion = prefs.getString(_versionKey); if (storedVersion == null) { // First install - no migration needed await prefs.setString(_versionKey, _currentVersion); return; } if (storedVersion != _currentVersion) { print('Migration needed: $storedVersion$_currentVersion'); await _runMigrations(db, storedVersion, _currentVersion); } } static Future<void> _runMigrations( InstantDB db, String fromVersion, String toVersion, ) async { try { final migrationManager = MigrationManager( fromVersion: fromVersion, toVersion: toVersion, ); await migrationManager.migrate(db); // Update stored version final prefs = await SharedPreferences.getInstance(); await prefs.setString(_versionKey, toVersion); print('Migration completed successfully'); } catch (e) { print('Migration failed: $e'); // Handle migration failure - might need manual intervention rethrow; } } } // Usage in your app initialization Future<void> initializeApp() async { final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig(syncEnabled: true), ); // Run migrations before using the database await AutoMigrationRunner.runMigrationsIfNeeded(db); runApp(MyApp(db: db)); }

Schema Migrations

Adding New Fields

Safely add new fields to existing entities:

class SchemaMigration { final InstantDB db; SchemaMigration(this.db); Future<void> addFieldToEntity({ required String entityType, required String fieldName, required dynamic defaultValue, }) async { print('Adding field $fieldName to $entityType entities'); // Query entities that don't have the new field final result = await db.queryOnce({entityType: {}}); final entities = (result.data?[entityType] as List? ?? []) .cast<Map<String, dynamic>>(); final operations = <Operation>[]; for (final entity in entities) { if (!entity.containsKey(fieldName)) { operations.add( db.update(entity['id'], {fieldName: defaultValue}), ); } } if (operations.isNotEmpty) { // Process in batches to avoid overwhelming the system await _processBatches(operations, batchSize: 50); print('Added $fieldName to ${operations.length} entities'); } else { print('All $entityType entities already have $fieldName field'); } } Future<void> _processBatches( List<Operation> operations, { int batchSize = 50, }) async { for (int i = 0; i < operations.length; i += batchSize) { final batch = operations.skip(i).take(batchSize).toList(); await db.transact(batch); // Small delay between batches to avoid overwhelming the system await Future.delayed(const Duration(milliseconds: 100)); print('Processed batch ${(i / batchSize).ceil() + 1}/${(operations.length / batchSize).ceil()}'); } } Future<void> removeFieldFromEntity({ required String entityType, required String fieldName, }) async { print('Removing field $fieldName from $entityType entities'); final result = await db.queryOnce({entityType: {}}); final entities = (result.data?[entityType] as List? ?? []) .cast<Map<String, dynamic>>(); final operations = <Operation>[]; for (final entity in entities) { if (entity.containsKey(fieldName)) { final updatedEntity = Map<String, dynamic>.from(entity); updatedEntity.remove(fieldName); operations.add( db.update(entity['id'], updatedEntity), ); } } if (operations.isNotEmpty) { await _processBatches(operations); print('Removed $fieldName from ${operations.length} entities'); } } Future<void> renameField({ required String entityType, required String oldFieldName, required String newFieldName, }) async { print('Renaming field $oldFieldName to $newFieldName in $entityType'); final result = await db.queryOnce({entityType: {}}); final entities = (result.data?[entityType] as List? ?? []) .cast<Map<String, dynamic>>(); final operations = <Operation>[]; for (final entity in entities) { if (entity.containsKey(oldFieldName) && !entity.containsKey(newFieldName)) { final value = entity[oldFieldName]; operations.add( db.update(entity['id'], { newFieldName: value, // Remove old field by setting it to null or omitting it }), ); } } if (operations.isNotEmpty) { await _processBatches(operations); print('Renamed field in ${operations.length} entities'); } } Future<void> transformFieldValues({ required String entityType, required String fieldName, required dynamic Function(dynamic oldValue) transformer, }) async { print('Transforming $fieldName values in $entityType'); final result = await db.queryOnce({entityType: {}}); final entities = (result.data?[entityType] as List? ?? []) .cast<Map<String, dynamic>>(); final operations = <Operation>[]; for (final entity in entities) { if (entity.containsKey(fieldName)) { final oldValue = entity[fieldName]; final newValue = transformer(oldValue); if (oldValue != newValue) { operations.add( db.update(entity['id'], {fieldName: newValue}), ); } } } if (operations.isNotEmpty) { await _processBatches(operations); print('Transformed $fieldName in ${operations.length} entities'); } } } // Usage examples Future<void> runSchemaMigrations(InstantDB db) async { final migration = SchemaMigration(db); // Add new field with default value await migration.addFieldToEntity( entityType: 'users', fieldName: 'lastLoginAt', defaultValue: 0, ); // Transform existing data await migration.transformFieldValues( entityType: 'posts', fieldName: 'createdAt', transformer: (oldValue) { // Convert string dates to timestamps if (oldValue is String) { return DateTime.parse(oldValue).millisecondsSinceEpoch; } return oldValue; }, ); // Rename field await migration.renameField( entityType: 'todos', oldFieldName: 'done', newFieldName: 'completed', ); }

Data Migrations

Complex Data Transformations

Handle complex data structure changes:

class DataMigrationRunner { final InstantDB db; DataMigrationRunner(this.db); Future<void> migrateUserProfiles() async { // Example: Migrate from flat user structure to nested profile print('Migrating user profiles to new structure'); final users = await db.queryOnce({'users': {}}); final userList = (users.data?['users'] as List? ?? []) .cast<Map<String, dynamic>>(); final operations = <Operation>[]; for (final user in userList) { // Check if migration is needed if (user.containsKey('firstName') && !user.containsKey('profile')) { final newProfile = { 'personal': { 'firstName': user['firstName'], 'lastName': user['lastName'], 'dateOfBirth': user['dateOfBirth'], }, 'contact': { 'email': user['email'], 'phone': user['phone'], }, 'preferences': { 'notifications': user['notifications'] ?? true, 'theme': user['theme'] ?? 'auto', 'language': user['language'] ?? 'en', }, }; // Create new structure and remove old fields final updatedUser = <String, dynamic>{ 'profile': newProfile, 'updatedAt': DateTime.now().millisecondsSinceEpoch, }; operations.add(db.update(user['id'], updatedUser)); } } if (operations.isNotEmpty) { await _processBatchOperations(operations); print('Migrated ${operations.length} user profiles'); } } Future<void> migratePostsWithTags() async { // Example: Migrate from tag strings to tag objects print('Migrating posts with tags'); final posts = await db.queryOnce({'posts': {}}); final postList = (posts.data?['posts'] as List? ?? []) .cast<Map<String, dynamic>>(); final operations = <Operation>[]; final tagMap = <String, String>{}; // tagName -> tagId for (final post in postList) { final tags = post['tags'] as List?; if (tags != null && tags.isNotEmpty && tags.first is String) { // Convert string tags to tag objects final newTags = <Map<String, dynamic>>[]; for (final tagName in tags.cast<String>()) { // Get or create tag ID if (!tagMap.containsKey(tagName)) { tagMap[tagName] = db.id(); // Create tag entity operations.add( ...db.create('tags', { 'id': tagMap[tagName]!, 'name': tagName, 'createdAt': DateTime.now().millisecondsSinceEpoch, }), ); } newTags.add({ 'id': tagMap[tagName]!, 'name': tagName, }); } // Update post with new tag structure operations.add( db.update(post['id'], { 'tags': newTags, 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ); } } if (operations.isNotEmpty) { await _processBatchOperations(operations); print('Migrated ${postList.length} posts and created ${tagMap.length} tags'); } } Future<void> _processBatchOperations( List<Operation> operations, { int batchSize = 25, }) async { for (int i = 0; i < operations.length; i += batchSize) { final batch = operations.skip(i).take(batchSize).toList(); try { await db.transact(batch); print('Processed batch ${(i / batchSize).floor() + 1}'); } catch (e) { print('Batch failed: $e'); // Decide whether to continue or abort rethrow; } // Small delay to avoid overwhelming the system await Future.delayed(const Duration(milliseconds: 200)); } } }

Rollback Strategies

Safe Migration with Rollback

Implement rollback capabilities for failed migrations:

class SafeMigrationRunner { final InstantDB db; final List<MigrationStep> _steps = []; SafeMigrationRunner(this.db); void addStep(MigrationStep step) { _steps.add(step); } Future<void> runWithRollback() async { final completedSteps = <MigrationStep>[]; try { for (final step in _steps) { print('Running migration step: ${step.name}'); await step.execute(db); completedSteps.add(step); print('Completed: ${step.name}'); } } catch (e) { print('Migration failed at step: ${completedSteps.length + 1}'); print('Rolling back...'); // Rollback completed steps in reverse order for (final step in completedSteps.reversed) { try { print('Rolling back: ${step.name}'); await step.rollback(db); } catch (rollbackError) { print('Rollback failed for ${step.name}: $rollbackError'); // Continue with other rollbacks } } rethrow; } } } abstract class MigrationStep { String get name; Future<void> execute(InstantDB db); Future<void> rollback(InstantDB db); } class AddFieldMigrationStep implements MigrationStep { final String entityType; final String fieldName; final dynamic defaultValue; AddFieldMigrationStep({ required this.entityType, required this.fieldName, required this.defaultValue, }); @override String get name => 'Add $fieldName to $entityType'; @override Future<void> execute(InstantDB db) async { final migration = SchemaMigration(db); await migration.addFieldToEntity( entityType: entityType, fieldName: fieldName, defaultValue: defaultValue, ); } @override Future<void> rollback(InstantDB db) async { final migration = SchemaMigration(db); await migration.removeFieldFromEntity( entityType: entityType, fieldName: fieldName, ); } } // Usage Future<void> runSafeMigration(InstantDB db) async { final runner = SafeMigrationRunner(db); runner.addStep(AddFieldMigrationStep( entityType: 'users', fieldName: 'profileVersion', defaultValue: 2, )); runner.addStep(CustomMigrationStep( name: 'Update user preferences', executeAction: (db) => _updateUserPreferences(db), rollbackAction: (db) => _revertUserPreferences(db), )); await runner.runWithRollback(); }

Testing Migrations

Migration Testing Framework

Create comprehensive tests for your migrations:

class MigrationTestSuite { late InstantDB testDb; Future<void> setUp() async { // Create test database instance testDb = await InstantDB.init( appId: 'test-app-id', config: const InstantConfig(syncEnabled: false), // Offline for testing ); } Future<void> tearDown() async { // Clean up test data await testDb.dispose(); } Future<void> testUserProfileMigration() async { // 1. Create test data in old format await _createOldFormatUsers(); // 2. Run migration final migration = DataMigrationRunner(testDb); await migration.migrateUserProfiles(); // 3. Verify migration results await _verifyUserProfileMigration(); } Future<void> _createOldFormatUsers() async { final operations = [ ...testDb.create('users', { 'id': testDb.id(), 'firstName': 'John', 'lastName': 'Doe', 'email': 'john@example.com', 'phone': '+1234567890', 'notifications': true, 'theme': 'dark', }), ...testDb.create('users', { 'id': testDb.id(), 'firstName': 'Jane', 'lastName': 'Smith', 'email': 'jane@example.com', 'phone': '+0987654321', 'notifications': false, 'theme': 'light', }), ]; await testDb.transact(operations); } Future<void> _verifyUserProfileMigration() async { final result = await testDb.queryOnce({'users': {}}); final users = (result.data?['users'] as List? ?? []) .cast<Map<String, dynamic>>(); for (final user in users) { // Verify new structure exists expect(user['profile'], isNotNull); expect(user['profile']['personal'], isNotNull); expect(user['profile']['contact'], isNotNull); expect(user['profile']['preferences'], isNotNull); // Verify data was migrated correctly final profile = user['profile'] as Map<String, dynamic>; expect(profile['personal']['firstName'], isNotEmpty); expect(profile['contact']['email'], contains('@')); // Verify old fields are removed expect(user.containsKey('firstName'), isFalse); expect(user.containsKey('lastName'), isFalse); } } Future<void> testMigrationRollback() async { // Test that rollback works correctly await _createOldFormatUsers(); final runner = SafeMigrationRunner(testDb); runner.addStep(AddFieldMigrationStep( entityType: 'users', fieldName: 'testField', defaultValue: 'test', )); // Force a failure in the second step to test rollback runner.addStep(FailingMigrationStep()); try { await runner.runWithRollback(); fail('Migration should have failed'); } catch (e) { // Verify rollback occurred final result = await testDb.queryOnce({'users': {}}); final users = (result.data?['users'] as List? ?? []) .cast<Map<String, dynamic>>(); for (final user in users) { expect(user.containsKey('testField'), isFalse); } } } } class FailingMigrationStep implements MigrationStep { @override String get name => 'Failing step'; @override Future<void> execute(InstantDB db) async { throw Exception('Intentional failure for testing'); } @override Future<void> rollback(InstantDB db) async { // Nothing to rollback } }

Best Practices

1. Version Your Migrations

class VersionedMigration { final String version; final String description; final Future<void> Function(InstantDB) migration; VersionedMigration({ required this.version, required this.description, required this.migration, }); } final migrations = [ VersionedMigration( version: '1.1.0', description: 'Add user preferences', migration: (db) => _addUserPreferences(db), ), VersionedMigration( version: '1.2.0', description: 'Migrate posts to new format', migration: (db) => _migratePostFormat(db), ), ];

2. Make Migrations Idempotent

Future<void> idempotentMigration(InstantDB db) async { // Check if migration already applied final users = await db.queryOnce({'users': {'limit': 1}}); final userList = (users.data?['users'] as List? ?? []); if (userList.isNotEmpty) { final firstUser = userList.first as Map<String, dynamic>; if (firstUser.containsKey('profileVersion')) { print('Migration already applied'); return; } } // Run migration await _actualMigration(db); }

3. Monitor Migration Performance

Future<void> monitoredMigration(InstantDB db) async { final stopwatch = Stopwatch()..start(); try { await _runMigration(db); print('Migration completed in ${stopwatch.elapsedMilliseconds}ms'); } catch (e) { print('Migration failed after ${stopwatch.elapsedMilliseconds}ms: $e'); rethrow; } }

4. Document Migration Impact

/// Migration: Add user preferences /// Impact: All existing users will get default preferences /// Rollback: Remove preferences field from all users /// Estimated time: ~30 seconds for 10k users /// Dependencies: None Future<void> documentedMigration(InstantDB db) async { // Implementation }

Next Steps

Learn more about maintaining robust InstantDB applications: