# Flutter InstantDB — Full Documentation > Flutter SDK for InstantDB — a real-time, offline-first, local-first database with reactive bindings. Type-safe InstaQL queries, optimistic InstaML transactions, WebSocket sync, auth, presence/rooms, file storage, and reactive widgets for iOS, Android, Web, macOS, Windows, and Linux. --- # Flutter InstantDB > Real-time, offline-first database for Flutter with reactive bindings, type-safe queries and code generation. Source: https://flutter-instantdb.vercel.app/docs A Flutter/Dart port of [InstantDB](https://instantdb.com) — a real-time, offline-first database with reactive bindings, a type-safe query DSL, code generation, and live multiplayer collaboration. Add the package and initialize the client. Reactive queries, transactions, and your first live widget. Compile-time-safe queries and writes generated from your models. Annotate a model, run the generator, get typed tables. ## A taste ```dart final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig(syncEnabled: true), ); final todos = await TodoTable() .query() .where((t) => t.done.eq(false)) .order((t) => t.createdAt.desc()) .getAll(db); await db.transact( TodoTable().tx(db).createModel( Todo(id: db.id(), title: 'Ship it', done: false), ), ); ``` --- # Migration Strategies > Upgrading Flutter InstantDB and handling data migrations Source: https://flutter-instantdb.vercel.app/docs/advanced/migration 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: ```dart // 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 backupCriticalData(InstantDB db) async { final criticalEntities = ['users', 'payments', 'orders']; final backup = >>{}; for (final entityType in criticalEntities) { final result = await db.queryOnce({entityType: {}}); backup[entityType] = (result.data?[entityType] as List? ?? []) .cast>(); } // Save backup to file or external storage await _saveBackup(backup); } Future _saveBackup(Map backup) async { // Implementation depends on your backup strategy // Could be local file, cloud storage, etc. } ``` ### Breaking Changes Migration Handle breaking changes systematically: ```dart class MigrationManager { final String fromVersion; final String toVersion; MigrationManager({ required this.fromVersion, required this.toVersion, }); Future 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 v1, List 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 _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 _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 _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>(); final operations = []; 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 _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 _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 _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: ```dart class AutoMigrationRunner { static const String _versionKey = 'instantdb_migration_version'; static const String _currentVersion = '1.0.0'; // Your current package version static Future 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 _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 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: ```dart class SchemaMigration { final InstantDB db; SchemaMigration(this.db); Future 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>(); final operations = []; 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 _processBatches( List 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 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>(); final operations = []; for (final entity in entities) { if (entity.containsKey(fieldName)) { final updatedEntity = Map.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 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>(); final operations = []; 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 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>(); final operations = []; 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 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: ```dart class DataMigrationRunner { final InstantDB db; DataMigrationRunner(this.db); Future 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>(); final operations = []; 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 = { '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 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>(); final operations = []; final tagMap = {}; // 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 = >[]; for (final tagName in tags.cast()) { // 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 _processBatchOperations( List 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: ```dart class SafeMigrationRunner { final InstantDB db; final List _steps = []; SafeMigrationRunner(this.db); void addStep(MigrationStep step) { _steps.add(step); } Future runWithRollback() async { final completedSteps = []; 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 execute(InstantDB db); Future 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 execute(InstantDB db) async { final migration = SchemaMigration(db); await migration.addFieldToEntity( entityType: entityType, fieldName: fieldName, defaultValue: defaultValue, ); } @override Future rollback(InstantDB db) async { final migration = SchemaMigration(db); await migration.removeFieldFromEntity( entityType: entityType, fieldName: fieldName, ); } } // Usage Future 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: ```dart class MigrationTestSuite { late InstantDB testDb; Future setUp() async { // Create test database instance testDb = await InstantDB.init( appId: 'test-app-id', config: const InstantConfig(syncEnabled: false), // Offline for testing ); } Future tearDown() async { // Clean up test data await testDb.dispose(); } Future 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 _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 _verifyUserProfileMigration() async { final result = await testDb.queryOnce({'users': {}}); final users = (result.data?['users'] as List? ?? []) .cast>(); 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; 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 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>(); for (final user in users) { expect(user.containsKey('testField'), isFalse); } } } } class FailingMigrationStep implements MigrationStep { @override String get name => 'Failing step'; @override Future execute(InstantDB db) async { throw Exception('Intentional failure for testing'); } @override Future rollback(InstantDB db) async { // Nothing to rollback } } ``` ## Best Practices ### 1. Version Your Migrations ```dart class VersionedMigration { final String version; final String description; final Future 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 ```dart Future 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; if (firstUser.containsKey('profileVersion')) { print('Migration already applied'); return; } } // Run migration await _actualMigration(db); } ``` ### 3. Monitor Migration Performance ```dart Future 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 ```dart /// 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 documentedMigration(InstantDB db) async { // Implementation } ``` ## Next Steps Learn more about maintaining robust InstantDB applications: - [Troubleshooting](/docs/advanced/troubleshooting) - Debug migration issues - [Performance Optimization](/docs/advanced/performance) - Optimize migration performance - [Offline Functionality](/docs/advanced/offline) - Handle migrations while offline - [API Reference](/docs/api) - Complete API documentation for migrations --- # Offline Functionality > Building offline-first applications with Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/advanced/offline Flutter InstantDB is designed with offline-first principles. Your app continues to work seamlessly when the network is unavailable, with automatic synchronization when connectivity is restored. ## Offline-First Architecture ### How It Works InstantDB's offline-first approach ensures your app remains functional regardless of network conditions: 1. **Local SQLite Storage**: All data is stored locally in SQLite 2. **Optimistic Updates**: Changes are applied immediately to the local database 3. **Sync Queue**: Mutations are queued for transmission when online 4. **Automatic Sync**: Changes sync automatically when connection is restored 5. **Conflict Resolution**: Server conflicts are resolved automatically ### Benefits - ✅ **Instant Responsiveness**: UI updates immediately, no waiting for server - ✅ **Reliable Offline Work**: Full functionality when disconnected - ✅ **Automatic Recovery**: Seamless sync when connection returns - ✅ **Conflict Resolution**: Handles concurrent edits gracefully - ✅ **Data Persistence**: Local data survives app restarts ## Handling Offline States ### Connection Status Monitoring Monitor and display connection status to users: ```dart class ConnectionAwareApp extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Watch((context) { final isOnline = db.syncEngine?.connectionStatus.value ?? false; return Scaffold( body: Column( children: [ // Connection status banner if (!isOnline) Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), color: Colors.orange, child: Row( children: [ const Icon(Icons.cloud_off, color: Colors.white, size: 16), const SizedBox(width: 8), const Text( 'Offline - Changes will sync when connected', style: TextStyle(color: Colors.white), ), const Spacer(), Text( 'Offline', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ], ), ), // Your app content Expanded(child: YourAppContent()), ], ), ); }); } } ``` ### Advanced Connection Status Widget Create a more sophisticated connection indicator: ```dart class AdvancedConnectionStatus extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Watch((context) { final isOnline = db.syncEngine?.connectionStatus.value ?? false; final pendingCount = _getPendingChangesCount(db); return AnimatedContainer( duration: const Duration(milliseconds: 300), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: _getStatusColor(isOnline, pendingCount), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _getStatusIcon(isOnline, pendingCount), size: 16, color: Colors.white, ), const SizedBox(width: 6), Text( _getStatusText(isOnline, pendingCount), style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500, ), ), if (pendingCount > 0) ...[ const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.white.withOpacity(0.3), borderRadius: BorderRadius.circular(8), ), child: Text( '$pendingCount', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ], ), ); }); } Color _getStatusColor(bool isOnline, int pendingCount) { if (!isOnline && pendingCount > 0) return Colors.orange; if (!isOnline) return Colors.red; if (pendingCount > 0) return Colors.blue; return Colors.green; } IconData _getStatusIcon(bool isOnline, int pendingCount) { if (!isOnline) return Icons.cloud_off; if (pendingCount > 0) return Icons.sync; return Icons.cloud_done; } String _getStatusText(bool isOnline, int pendingCount) { if (!isOnline && pendingCount > 0) return 'Offline - $pendingCount pending'; if (!isOnline) return 'Offline'; if (pendingCount > 0) return 'Syncing'; return 'Online'; } int _getPendingChangesCount(InstantDB db) { // This would need to be implemented in the sync engine // For now, return a placeholder return 0; } } ``` ## Offline Data Patterns ### Optimistic Updates All mutations in InstantDB are optimistic by default: ```dart class OptimisticTodoList extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return InstantBuilder( query: {'todos': {'orderBy': {'createdAt': 'desc'}}}, builder: (context, result) { final todos = (result.data?['todos'] as List? ?? []) .cast>(); return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return TodoItem( todo: todo, onToggle: () => _toggleTodo(db, todo), onDelete: () => _deleteTodo(db, todo['id']), ); }, ); }, ); } Future _toggleTodo(InstantDB db, Map todo) async { // This update happens immediately in the UI // Sync happens automatically in the background await db.transact([ db.update(todo['id'], { 'completed': !todo['completed'], 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); } Future _deleteTodo(InstantDB db, String todoId) async { // Deletion also happens immediately await db.transact([ db.delete(todoId), ]); } } ``` ### Offline-Aware CRUD Operations Create operations that provide feedback for offline states: ```dart class OfflineAwareCRUD { final InstantDB db; OfflineAwareCRUD(this.db); Future createTodo({ required String text, bool showOfflineMessage = true, }) async { try { final todoId = db.id(); await db.transact([ ...db.create('todos', { 'id': todoId, 'text': text, 'completed': false, 'createdAt': DateTime.now().millisecondsSinceEpoch, }), ]); final isOnline = db.syncEngine?.connectionStatus.value ?? false; return OperationResult.success( message: isOnline ? 'Todo created and synced' : 'Todo created - will sync when online', data: {'id': todoId}, ); } catch (e) { return OperationResult.error('Failed to create todo: $e'); } } Future updateTodo({ required String id, required Map updates, }) async { try { await db.transact([ db.update(id, { ...updates, 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); final isOnline = db.syncEngine?.connectionStatus.value ?? false; return OperationResult.success( message: isOnline ? 'Todo updated and synced' : 'Todo updated - will sync when online', ); } catch (e) { return OperationResult.error('Failed to update todo: $e'); } } Future deleteTodo(String id) async { try { await db.transact([db.delete(id)]); final isOnline = db.syncEngine?.connectionStatus.value ?? false; return OperationResult.success( message: isOnline ? 'Todo deleted and synced' : 'Todo deleted - will sync when online', ); } catch (e) { return OperationResult.error('Failed to delete todo: $e'); } } } class OperationResult { final bool success; final String message; final Map? data; OperationResult._({ required this.success, required this.message, this.data, }); factory OperationResult.success(String message, {Map? data}) { return OperationResult._(success: true, message: message, data: data); } factory OperationResult.error(String message) { return OperationResult._(success: false, message: message); } } ``` ## Conflict Resolution ### Understanding Conflicts Conflicts occur when the same data is modified by multiple clients while offline: ```dart class ConflictResolutionExample extends StatefulWidget { @override State createState() => _ConflictResolutionExampleState(); } class _ConflictResolutionExampleState extends State { String? _conflictMessage; @override void initState() { super.initState(); _monitorConflicts(); } void _monitorConflicts() { final db = InstantProvider.of(context); // Listen for sync events (this is conceptual - actual API may vary) db.syncEngine?.onSyncEvent.listen((event) { if (event.type == 'conflict_resolved') { setState(() { _conflictMessage = 'Conflict resolved: ${event.description}'; }); // Clear message after 5 seconds Timer(const Duration(seconds: 5), () { if (mounted) { setState(() { _conflictMessage = null; }); } }); } }); } @override Widget build(BuildContext context) { return Column( children: [ if (_conflictMessage != null) Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.info, color: Colors.blue.shade700), const SizedBox(width: 8), Expanded( child: Text( _conflictMessage!, style: TextStyle(color: Colors.blue.shade700), ), ), ], ), ), // Your main content Expanded(child: YourContent()), ], ); } } ``` ### Custom Conflict Resolution Implement custom logic for handling specific conflict scenarios: ```dart class CustomConflictResolver { static Map resolveDocumentConflict({ required Map localVersion, required Map serverVersion, required String userId, }) { // Last-write-wins with user preference final localTimestamp = localVersion['updatedAt'] as int? ?? 0; final serverTimestamp = serverVersion['updatedAt'] as int? ?? 0; // If local is newer, keep local if (localTimestamp > serverTimestamp) { return localVersion; } // If server is newer, merge intelligently final resolved = Map.from(serverVersion); // Keep local changes for specific fields if user is the author if (localVersion['authorId'] == userId) { final preserveFields = ['title', 'content', 'tags']; for (final field in preserveFields) { if (localVersion.containsKey(field)) { resolved[field] = localVersion[field]; } } } // Add conflict resolution metadata resolved['_conflictResolved'] = true; resolved['_conflictResolvedAt'] = DateTime.now().millisecondsSinceEpoch; resolved['_conflictResolvedBy'] = 'user_preference'; return resolved; } static List mergeArrayConflict({ required List localArray, required List serverArray, }) { // Merge arrays preserving unique items final merged = []; final seen = {}; // Add all server items first for (final item in serverArray) { if (!seen.contains(item)) { merged.add(item); seen.add(item); } } // Add local items that aren't in server for (final item in localArray) { if (!seen.contains(item)) { merged.add(item); seen.add(item); } } return merged; } } ``` ## Offline Authentication ### Cached Authentication Handle authentication when offline: ```dart class OfflineAuthManager { final InstantDB db; OfflineAuthManager(this.db); Future getCachedUser() async { try { // Try to get current user (may use cached data) final user = db.auth.currentUser.value; if (user != null) return user; // If no current user, try to restore from secure storage final token = await SecureStorage.getAuthToken(); if (token != null) { try { return await db.auth.signInWithToken(token); } catch (e) { // Token might be expired, handle gracefully print('Failed to restore auth with cached token: $e'); } } return null; } catch (e) { print('Error getting cached user: $e'); return null; } } bool canPerformOfflineAuth() { // Check if user has valid cached credentials return db.auth.isAuthenticated; } Future scheduleAuthRefresh() async { // Schedule auth refresh for when connection is restored final isOnline = db.syncEngine?.connectionStatus.value ?? false; if (!isOnline) { // Listen for connection restoration db.syncEngine?.connectionStatus.stream.listen((connected) { if (connected) { _refreshAuthWhenOnline(); } }); } else { await _refreshAuthWhenOnline(); } } Future _refreshAuthWhenOnline() async { try { await db.auth.refreshUser(); } catch (e) { print('Auth refresh failed: $e'); // Handle auth refresh failure (e.g., redirect to login) } } } class SecureStorage { static Future getAuthToken() async { // Implementation depends on your secure storage solution // e.g., flutter_secure_storage return null; } } ``` ## Offline UI Patterns ### Offline-First Form Handling Create forms that work seamlessly offline: ```dart class OfflineForm extends StatefulWidget { final Map? initialData; final String entityType; const OfflineForm({ super.key, this.initialData, required this.entityType, }); @override State createState() => _OfflineFormState(); } class _OfflineFormState extends State { final _formKey = GlobalKey(); late final Map _formData; bool _isSaving = false; String? _saveMessage; @override void initState() { super.initState(); _formData = Map.from(widget.initialData ?? {}); } @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Watch((context) { final isOnline = db.syncEngine?.connectionStatus.value ?? false; return Form( key: _formKey, child: Column( children: [ // Offline indicator in form if (!isOnline) Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.amber.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.amber.shade300), ), child: Row( children: [ Icon(Icons.info, color: Colors.amber.shade700, size: 20), const SizedBox(width: 8), Expanded( child: Text( 'You\'re offline. Changes will be saved locally and synced when connected.', style: TextStyle(color: Colors.amber.shade700), ), ), ], ), ), const SizedBox(height: 16), // Form fields ...buildFormFields(), const SizedBox(height: 24), // Save message if (_saveMessage != null) Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( _saveMessage!, style: TextStyle(color: Colors.green.shade700), ), ), const SizedBox(height: 16), // Save button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isSaving ? null : _saveForm, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_isSaving) ...[ const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 8), ], Text(_isSaving ? 'Saving...' : 'Save'), if (!isOnline && !_isSaving) ...[ const SizedBox(width: 8), Icon(Icons.offline_pin, size: 16), ], ], ), ), ), ], ), ); }); } List buildFormFields() { // Build your form fields based on entity type return [ TextFormField( initialValue: _formData['title']?.toString(), decoration: const InputDecoration(labelText: 'Title'), onSaved: (value) => _formData['title'] = value, validator: (value) => value?.isEmpty ?? true ? 'Title is required' : null, ), const SizedBox(height: 16), TextFormField( initialValue: _formData['description']?.toString(), decoration: const InputDecoration(labelText: 'Description'), maxLines: 3, onSaved: (value) => _formData['description'] = value, ), ]; } Future _saveForm() async { if (!_formKey.currentState!.validate()) return; _formKey.currentState!.save(); setState(() { _isSaving = true; _saveMessage = null; }); try { final db = InstantProvider.of(context); final isOnline = db.syncEngine?.connectionStatus.value ?? false; final isUpdate = _formData.containsKey('id'); if (isUpdate) { await db.transact([ db.update(_formData['id'], { ..._formData, 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); } else { await db.transact([ ...db.create(widget.entityType, { 'id': db.id(), ..._formData, 'createdAt': DateTime.now().millisecondsSinceEpoch, }), ]); } setState(() { _saveMessage = isOnline ? '${isUpdate ? 'Updated' : 'Created'} successfully and synced' : '${isUpdate ? 'Updated' : 'Created'} successfully - will sync when online'; }); // Clear message after 3 seconds Timer(const Duration(seconds: 3), () { if (mounted) { setState(() { _saveMessage = null; }); } }); } catch (e) { setState(() { _saveMessage = 'Error: $e'; }); } finally { setState(() { _isSaving = false; }); } } } ``` ## Best Practices ### 1. Embrace Optimistic Updates Trust InstantDB's optimistic update system: ```dart // Good: Let InstantDB handle optimistic updates await db.transact([db.update(itemId, newData)]); // Avoid: Manual optimistic updates setState(() { localData = newData; // Don't do this - let InstantDB handle it }); await db.transact([db.update(itemId, newData)]); ``` ### 2. Provide Clear Offline Feedback Always inform users about offline status: ```dart class OfflineFeedback extends StatelessWidget { final Widget child; const OfflineFeedback({super.key, required this.child}); @override Widget build(BuildContext context) { return Column( children: [ ConnectionStatusBar(), Expanded(child: child), ], ); } } ``` ### 3. Handle Large Offline Datasets Consider data size when working offline: ```dart class OfflineDataManager { static const int MAX_OFFLINE_ITEMS = 1000; static Future preloadOfflineData(InstantDB db) async { // Load essential data for offline use final recentPosts = await db.queryOnce({ 'posts': { 'where': {'createdAt': {'\$gte': _getLastWeekTimestamp()}}, 'limit': MAX_OFFLINE_ITEMS, 'orderBy': {'createdAt': 'desc'}, } }); // Data is now cached locally } static int _getLastWeekTimestamp() { return DateTime.now() .subtract(const Duration(days: 7)) .millisecondsSinceEpoch; } } ``` ### 4. Test Offline Scenarios Thoroughly test offline functionality: ```dart void testOfflineScenarios() { group('Offline functionality', () { late InstantDB db; setUp(() async { db = await InstantDB.init( appId: 'test-app', config: const InstantConfig(syncEnabled: true), ); }); test('should create items when offline', () async { // Simulate offline mode await db.syncEngine?.disconnect(); // Create item await db.transact([ ...db.create('items', { 'id': db.id(), 'name': 'Offline Item', }), ]); // Verify item exists locally final result = await db.queryOnce({'items': {}}); expect(result.data?['items'], hasLength(1)); }); test('should sync when coming back online', () async { // Test sync recovery await db.syncEngine?.connect(); // Wait for sync await Future.delayed(const Duration(seconds: 1)); // Verify sync occurred // Implementation depends on sync monitoring capabilities }); }); } ``` ## Next Steps Learn more about advanced InstantDB features: - [Performance Optimization](/docs/advanced/performance) - Optimizing offline performance - [Troubleshooting](/docs/advanced/troubleshooting) - Debugging offline issues - [Migration Strategies](/docs/advanced/migration) - Upgrading offline-enabled apps - [Real-time Sync](/docs/realtime/sync) - Understanding sync mechanisms --- # Performance Optimization > Optimizing Flutter InstantDB applications for best performance Source: https://flutter-instantdb.vercel.app/docs/advanced/performance Optimize your Flutter InstantDB applications for maximum performance with efficient queries, smart caching strategies, and optimized UI patterns. ## Query Optimization ### Efficient Query Patterns Write queries that minimize data transfer and processing: ```dart // ✅ Good: Specific queries with filters final recentPosts = db.subscribeQuery({ 'posts': { 'where': { 'published': true, 'createdAt': {'\$gte': DateTime.now().subtract(Duration(days: 7)).millisecondsSinceEpoch} }, 'orderBy': {'createdAt': 'desc'}, 'limit': 20, } }); // ❌ Avoid: Broad queries without filters final allPosts = db.subscribeQuery({'posts': {}}); ``` ### Pagination for Large Datasets Implement efficient pagination to handle large amounts of data: ```dart class PaginatedList extends StatefulWidget { final String entityType; final Map? whereClause; final int pageSize; const PaginatedList({ super.key, required this.entityType, this.whereClause, this.pageSize = 20, }); @override State createState() => _PaginatedListState(); } class _PaginatedListState extends State { int _currentPage = 0; final List> _allItems = []; bool _isLoading = false; bool _hasMore = true; @override void initState() { super.initState(); _loadPage(0); } Future _loadPage(int page) async { if (_isLoading || !_hasMore) return; setState(() { _isLoading = true; }); try { final db = InstantProvider.of(context); final result = await db.queryOnce({ widget.entityType: { if (widget.whereClause != null) 'where': widget.whereClause, 'orderBy': {'createdAt': 'desc'}, 'limit': widget.pageSize, 'offset': page * widget.pageSize, } }); final newItems = (result.data?[widget.entityType] as List? ?? []) .cast>(); setState(() { if (page == 0) { _allItems.clear(); } _allItems.addAll(newItems); _hasMore = newItems.length == widget.pageSize; _currentPage = page; }); } finally { setState(() { _isLoading = false; }); } } @override Widget build(BuildContext context) { return ListView.builder( itemCount: _allItems.length + (_hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == _allItems.length) { // Load more indicator if (!_isLoading && _hasMore) { // Trigger load more WidgetsBinding.instance.addPostFrameCallback((_) { _loadPage(_currentPage + 1); }); } return const Center( child: Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator(), ), ); } final item = _allItems[index]; return ListTile( title: Text(item['title'] ?? ''), subtitle: Text(item['description'] ?? ''), ); }, ); } } ``` ### Query Result Caching Implement smart caching for frequently accessed data: ```dart class QueryCache { static final Map _cache = {}; static const Duration _defaultTTL = Duration(minutes: 5); static Signal? getCached( String queryKey, Map query, ) { final cached = _cache[queryKey]; if (cached != null && !cached.isExpired) { return cached.signal; } return null; } static void setCached( String queryKey, Signal signal, { Duration? ttl, }) { _cache[queryKey] = CachedQuery( signal: signal, expiresAt: DateTime.now().add(ttl ?? _defaultTTL), ); } static void clearExpired() { _cache.removeWhere((_, cached) => cached.isExpired); } static void clear() { _cache.clear(); } } class CachedQuery { final Signal signal; final DateTime expiresAt; CachedQuery({required this.signal, required this.expiresAt}); bool get isExpired => DateTime.now().isAfter(expiresAt); } // Cached query widget class CachedInstantBuilder extends StatelessWidget { final Map query; final String? cacheKey; final Duration? cacheTTL; final Widget Function(BuildContext, Map?) builder; const CachedInstantBuilder({ super.key, required this.query, this.cacheKey, this.cacheTTL, required this.builder, }); @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final key = cacheKey ?? query.toString(); // Try to get from cache first var querySignal = QueryCache.getCached(key, query); if (querySignal == null) { // Create new query and cache it querySignal = db.subscribeQuery(query); QueryCache.setCached(key, querySignal, ttl: cacheTTL); } return Watch((context) { final result = querySignal!.value; return builder(context, result.data); }); } } ``` ## Memory Management ### Efficient Widget Patterns Optimize widget rebuilds and memory usage: ```dart // ✅ Good: Specific data extraction class OptimizedPostList extends StatelessWidget { @override Widget build(BuildContext context) { return InstantBuilderTyped>( query: { 'posts': { 'where': {'published': true}, 'limit': 50, } }, transformer: (data) => (data['posts'] as List) .map((json) => Post.fromJson(json)) .toList(), builder: (context, posts) { return ListView.builder( itemCount: posts.length, itemBuilder: (context, index) => PostCard(post: posts[index]), ); }, ); } } // ❌ Avoid: Processing data in build method class UnoptimizedPostList extends StatelessWidget { @override Widget build(BuildContext context) { return InstantBuilder( query: {'posts': {}}, builder: (context, data) { // Expensive operations in build method final posts = (data['posts'] as List) .where((p) => p['published'] == true) .map((json) => Post.fromJson(json)) .toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return ListView.builder( itemCount: posts.length, itemBuilder: (context, index) => PostCard(post: posts[index]), ); }, ); } } ``` ### Memory-Efficient List Rendering Use ListView.builder for large datasets: ```dart class LargeDataList extends StatelessWidget { @override Widget build(BuildContext context) { return InstantBuilder( query: { 'items': { 'orderBy': {'createdAt': 'desc'}, 'limit': 1000, // Large dataset } }, builder: (context, result) { final items = (result.data?['items'] as List? ?? []) .cast>(); return ListView.builder( // Only builds visible items itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return ListTile( key: Key(item['id']), // Important for performance title: Text(item['title']), subtitle: Text(item['description']), ); }, ); }, ); } } ``` ### Dispose Resources Properly Always clean up resources to prevent memory leaks: ```dart class ResourceAwareWidget extends StatefulWidget { @override State createState() => _ResourceAwareWidgetState(); } class _ResourceAwareWidgetState extends State { late final Signal _querySignal; StreamSubscription? _subscription; Timer? _refreshTimer; @override void initState() { super.initState(); final db = InstantProvider.of(context); _querySignal = db.subscribeQuery({'items': {}}); // Listen to query changes _subscription = _querySignal.toStream().listen((result) { // Handle query updates }); // Periodic refresh _refreshTimer = Timer.periodic( const Duration(minutes: 5), (_) => _refreshData(), ); } @override void dispose() { // Clean up all resources _subscription?.cancel(); _refreshTimer?.cancel(); super.dispose(); } void _refreshData() { // Refresh implementation } @override Widget build(BuildContext context) { return Watch((context) { final result = _querySignal.value; return YourWidget(data: result.data); }); } } ``` ## Sync Performance ### Batch Operations Efficiently Group related operations for better sync performance: ```dart class BatchOperationManager { final InstantDB db; final List _pendingOperations = []; Timer? _batchTimer; static const Duration _batchDelay = Duration(milliseconds: 500); static const int _maxBatchSize = 50; BatchOperationManager(this.db); void addOperation(Operation operation) { _pendingOperations.add(operation); if (_pendingOperations.length >= _maxBatchSize) { _flushBatch(); } else { _scheduleBatch(); } } void _scheduleBatch() { _batchTimer?.cancel(); _batchTimer = Timer(_batchDelay, _flushBatch); } Future _flushBatch() async { if (_pendingOperations.isEmpty) return; final batch = List.from(_pendingOperations); _pendingOperations.clear(); _batchTimer?.cancel(); try { await db.transact(batch); } catch (e) { // Handle batch error - could retry or log print('Batch operation failed: $e'); } } void dispose() { _batchTimer?.cancel(); if (_pendingOperations.isNotEmpty) { _flushBatch(); } } } // Usage class BatchedCRUD { final BatchOperationManager _batchManager; BatchedCRUD(InstantDB db) : _batchManager = BatchOperationManager(db); void createMultipleItems(List> items) { for (final item in items) { _batchManager.addOperation( db.create('items', { 'id': db.id(), ...item, }).first, ); } } void updateMultipleItems(List>> updates) { for (final update in updates) { _batchManager.addOperation( db.update(update.key, update.value), ); } } } ``` ### Connection Pool Management Optimize WebSocket connections: ```dart class ConnectionManager { static const Duration _reconnectDelay = Duration(seconds: 2); static const Duration _heartbeatInterval = Duration(seconds: 30); static const int _maxReconnectAttempts = 5; final InstantDB db; int _reconnectAttempts = 0; Timer? _heartbeatTimer; ConnectionManager(this.db) { _startHeartbeat(); _monitorConnection(); } void _startHeartbeat() { _heartbeatTimer?.cancel(); _heartbeatTimer = Timer.periodic(_heartbeatInterval, (_) { _sendHeartbeat(); }); } void _sendHeartbeat() { // Implementation depends on sync engine capabilities // This is conceptual try { db.syncEngine?.ping(); _reconnectAttempts = 0; // Reset on successful ping } catch (e) { print('Heartbeat failed: $e'); _handleConnectionLoss(); } } void _monitorConnection() { db.syncEngine?.connectionStatus.stream.listen((isConnected) { if (!isConnected) { _handleConnectionLoss(); } else { _handleConnectionRestored(); } }); } void _handleConnectionLoss() { if (_reconnectAttempts < _maxReconnectAttempts) { Timer(_reconnectDelay * (_reconnectAttempts + 1), () { _attemptReconnect(); }); } else { print('Max reconnect attempts reached'); } } void _handleConnectionRestored() { _reconnectAttempts = 0; _startHeartbeat(); } void _attemptReconnect() { _reconnectAttempts++; try { db.syncEngine?.connect(); } catch (e) { print('Reconnect attempt $_reconnectAttempts failed: $e'); } } void dispose() { _heartbeatTimer?.cancel(); } } ``` ## UI Performance ### Optimized Presence Updates Throttle presence updates to avoid excessive network traffic: ```dart class ThrottledPresenceManager { final InstantRoom room; Timer? _cursorTimer; Timer? _typingTimer; static const Duration _cursorThrottle = Duration(milliseconds: 100); static const Duration _typingDebounce = Duration(milliseconds: 300); ThrottledPresenceManager(this.room); void updateCursor(double x, double y) { _cursorTimer?.cancel(); _cursorTimer = Timer(_cursorThrottle, () { room.updateCursor(x: x, y: y); }); } void setTyping(bool isTyping) { _typingTimer?.cancel(); if (isTyping) { room.setTyping(true); _typingTimer = Timer(_typingDebounce, () { room.setTyping(false); }); } else { room.setTyping(false); } } void dispose() { _cursorTimer?.cancel(); _typingTimer?.cancel(); } } ``` ### Efficient Cursor Rendering Optimize cursor rendering for many users: ```dart class OptimizedCursorLayer extends StatelessWidget { final InstantRoom room; final Widget child; static const int _maxVisibleCursors = 10; const OptimizedCursorLayer({ super.key, required this.room, required this.child, }); @override Widget build(BuildContext context) { return Stack( children: [ child, Watch((context) { final cursors = room.getCursors().value; // Limit visible cursors for performance final visibleCursors = _getLimitedCursors(cursors); return RepaintBoundary( child: Stack( children: visibleCursors.map((entry) { return Positioned( left: entry.value.x, top: entry.value.y, child: CursorWidget( key: Key(entry.key), cursor: entry.value, ), ); }).toList(), ), ); }), ], ); } List> _getLimitedCursors( Map allCursors, ) { final entries = allCursors.entries.toList(); // Sort by last update time entries.sort((a, b) { final aTime = a.value.data['lastUpdate'] ?? 0; final bTime = b.value.data['lastUpdate'] ?? 0; return bTime.compareTo(aTime); }); return entries.take(_maxVisibleCursors).toList(); } } class CursorWidget extends StatelessWidget { final dynamic cursor; const CursorWidget({super.key, required this.cursor}); @override Widget build(BuildContext context) { // Wrap in RepaintBoundary for better performance return RepaintBoundary( child: Container( width: 20, height: 20, decoration: BoxDecoration( color: cursor.data['color'] ?? Colors.blue, shape: BoxShape.circle, ), ), ); } } ``` ## Monitoring and Analytics ### Performance Monitoring Track performance metrics: ```dart class PerformanceMonitor { static final Map _metrics = {}; static void startTimer(String operation) { _metrics[operation] = PerformanceMetric( operation: operation, startTime: DateTime.now(), ); } static void endTimer(String operation) { final metric = _metrics[operation]; if (metric != null) { metric.endTime = DateTime.now(); _logMetric(metric); } } static void _logMetric(PerformanceMetric metric) { final duration = metric.duration; print('Performance: ${metric.operation} took ${duration.inMilliseconds}ms'); // Send to analytics service _sendToAnalytics(metric); } static void _sendToAnalytics(PerformanceMetric metric) { // Implementation for your analytics service // e.g., Firebase Performance, custom analytics } static Future measureAsync( String operation, Future Function() action, ) async { startTimer(operation); try { return await action(); } finally { endTimer(operation); } } static T measure( String operation, T Function() action, ) { startTimer(operation); try { return action(); } finally { endTimer(operation); } } } class PerformanceMetric { final String operation; final DateTime startTime; DateTime? endTime; PerformanceMetric({ required this.operation, required this.startTime, }); Duration get duration => endTime!.difference(startTime); } // Usage class PerformanceAwareService { Future performExpensiveOperation() async { await PerformanceMonitor.measureAsync('expensive_operation', () async { // Your expensive operation here await Future.delayed(const Duration(seconds: 2)); }); } } ``` ### Memory Usage Tracking Monitor memory consumption: ```dart class MemoryMonitor { static void logMemoryUsage(String context) { // Get current memory usage (implementation varies by platform) final rss = _getCurrentMemoryUsage(); print('Memory usage at $context: ${rss}MB'); if (rss > _getMemoryThreshold()) { print('WARNING: High memory usage detected'); _handleHighMemoryUsage(); } } static double _getCurrentMemoryUsage() { // Implementation depends on platform // This is a placeholder return 0.0; } static double _getMemoryThreshold() { // Define your memory threshold (e.g., 200MB) return 200.0; } static void _handleHighMemoryUsage() { // Clear caches, dispose unused resources, etc. QueryCache.clear(); _forceGarbageCollection(); } static void _forceGarbageCollection() { // Force garbage collection if supported // Implementation varies by platform } } ``` ## Best Practices ### 1. Use Specific Queries Always filter data at the query level: ```dart // ✅ Good: Filter in query final activeUsers = db.subscribeQuery({ 'users': { 'where': {'status': 'active'}, 'limit': 100, } }); // ❌ Avoid: Filter in UI final allUsers = db.subscribeQuery({'users': {}}); // Then filtering in build method ``` ### 2. Implement Proper Pagination Don't load all data at once: ```dart // ✅ Good: Paginated loading void loadNextPage() { final offset = currentPage * pageSize; final query = { 'items': { 'limit': pageSize, 'offset': offset, } }; } ``` ### 3. Batch Related Operations Group operations for efficiency: ```dart // ✅ Good: Batch operations await db.transact([ ...db.create('post', postData), db.update(authorId, {'postCount': {'\$increment': 1}}), ...db.create('notification', notificationData), ]); // ❌ Avoid: Separate transactions await db.transact([...db.create('post', postData)]); await db.transact([db.update(authorId, {'postCount': {'\$increment': 1}})]); await db.transact([...db.create('notification', notificationData)]); ``` ### 4. Use Keys for List Items Always provide keys for dynamic lists: ```dart ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return ListTile( key: Key(item['id']), // Important for performance title: Text(item['title']), ); }, ) ``` ### 5. Profile and Measure Regularly profile your app: ```dart void main() { // Enable performance overlay in debug mode if (kDebugMode) { debugProfileBuildsEnabled = true; } runApp(MyApp()); } ``` ## Performance Checklist Use this checklist to ensure optimal performance: - [ ] Queries use specific filters and limits - [ ] Large lists use ListView.builder with keys - [ ] Resources are properly disposed - [ ] Batch operations are used where possible - [ ] Presence updates are throttled - [ ] Memory usage is monitored - [ ] Performance metrics are tracked - [ ] Caching is implemented appropriately - [ ] UI rebuilds are minimized - [ ] Network requests are optimized ## Next Steps Learn more about optimizing your Flutter InstantDB app: - [Troubleshooting](/docs/advanced/troubleshooting) - Debugging performance issues - [Migration Strategies](/docs/advanced/migration) - Upgrading performant apps - [Offline Functionality](/docs/advanced/offline) - Optimizing offline performance - [Real-time Sync](/docs/realtime/sync) - Sync performance optimization --- # Schema CLI (TS ⇆ Dart) > Convert instant.schema.ts to and from Dart @InstantModel classes Source: https://flutter-instantdb.vercel.app/docs/advanced/schema-cli The `schema` CLI (`bin/schema.dart`) converts between InstantDB's `instant.schema.ts` and Dart `@InstantModel` classes — the input to the code generator. It is pure Dart (no analyzer, no extra dependencies). ## Commands ```bash # Cloud round-trips (wrap instant-cli) dart run instantdb_flutter:schema pull # instant-cli pull → convert TS to Dart dart run instantdb_flutter:schema push # convert Dart to TS → instant-cli push # Offline conversion (no cloud / npx) dart run instantdb_flutter:schema to-dart instant.schema.ts -s lib/schema/app_schema.dart dart run instantdb_flutter:schema to-ts -s lib/schema/app_schema.dart # Best-effort normalized diff of the Dart schema vs instant.schema.ts dart run instantdb_flutter:schema diff -s lib/schema/app_schema.dart ``` After `pull`/`to-dart`, run `dart run build_runner build` to regenerate the typed tables. ## Type mapping | `instant.schema.ts` | Dart | Notes | |---------------------|-------------------------|-------| | `i.string()` | `String` | | | `i.number()` | `num` | `int`/`double`/`num` all collapse to `i.number()` on export | | `i.boolean()` | `bool` | | | `i.json()` | `Map?` | always nullable + optional ctor param | | `i.date()` | `DateTime?` | always nullable + optional ctor param | - `.optional()` → nullable Dart type + optional constructor parameter. - Every entity gets a required `final String id`. - `$`-prefixed system entities (`$users`, `$files`) are not emitted as Dart classes; they only resolve as link targets. ## Modifiers and constraints `@InstantField` carries `unique` / `indexed` so constraints survive the round trip: ```dart @InstantModel('users') class User { final String id; @InstantField('email', unique: true, indexed: true) final String email; const User({required this.id, required this.email}); } ``` emits `email: i.string().unique().indexed()`. The code generator ignores these flags. ## Links TS `links` (forward/reverse) map to paired `@InstantLink` fields: - `has: 'one'` → `T?` - `has: 'many'` → `List` The side of a link that lands on a system entity is skipped. On export, reciprocal links are deduped and a `has: 'many'` reverse is synthesized when only one side is declared — so hand-tuned link names may change. --- # Files & Storage > Upload, download, and delete files with db.storage and the $files namespace Source: https://flutter-instantdb.vercel.app/docs/advanced/storage InstantDB includes a storage API for uploading and serving files, plus a queryable `$files` namespace for file metadata. ## The storage client `db.storage` is an `InstantStorage` instance exposing three methods. ### `uploadFile` Upload bytes to a path. Returns the created `InstantFile` record. ```dart import 'dart:typed_data'; final bytes = Uint8List.fromList(/* ... */); final file = await db.storage.uploadFile( 'avatars/ada.png', bytes, contentType: 'image/png', // optional contentDisposition: 'inline', // optional ); print(file.path); // 'avatars/ada.png' ``` ### `getDownloadUrl` Get a signed download URL for the file at a path. ```dart final url = await db.storage.getDownloadUrl('avatars/ada.png'); // Use `url` in an Image.network(...) or share it ``` ### `delete` Delete the file at a path. ```dart await db.storage.delete('avatars/ada.png'); ``` ### `list` List uploaded files by reading the `$files` namespace through the query pipeline. Pass `where`, `order`, `limit`, and `offset` to filter and page. Requires sync to be enabled so the `$files` namespace is populated. ```dart Future> list({ Map? where, Map? order, int? limit, int? offset, }); ``` ```dart final files = await db.storage.list( order: {'serverCreatedAt': 'desc'}, limit: 50, ); for (final f in files) { print('${f.path} (${f.size} bytes)'); } ``` ## The `InstantFile` model `uploadFile` returns an `InstantFile` describing the stored file: ```dart class InstantFile { final String id; final String path; final String? url; final int? size; final String? contentType; } ``` ## Querying `$files` The `$files` namespace is queryable like any other namespace: ```dart final result = await db.queryOnce({r'$files': {}}); final files = result.data![r'$files'] as List; ``` A local file reference can be removed via a transaction on the `$files` namespace: ```dart await db.transact(db.tx[r'$files'][fileId].delete()); ``` --- # Troubleshooting > Common issues and debugging techniques for Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/advanced/troubleshooting Debug and resolve common issues with Flutter InstantDB applications using systematic troubleshooting techniques and diagnostic tools. ## Common Issues ### Connection and Sync Problems #### 1. "Failed to connect to InstantDB server" **Symptoms:** App shows offline status, sync doesn't work **Causes & Solutions:** ```dart // Check your app ID and configuration final db = await InstantDB.init( appId: 'your-correct-app-id', // Verify this is correct config: const InstantConfig( syncEnabled: true, // Use custom endpoint if needed baseUrl: 'https://api.instantdb.com', // Default websocketUrl: 'wss://api.instantdb.com/ws', // Default ), ); // Enable verbose logging to see connection details final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, verboseLogging: true, // Enable detailed logs ), ); ``` **Debugging steps:** 1. Check your internet connection 2. Verify app ID is correct 3. Check firewall/proxy settings 4. Enable verbose logging to see detailed error messages #### 2. "Data not syncing between devices" **Symptoms:** Changes on one device don't appear on others **Common causes:** ```dart // ❌ Problem: Different app IDs Device1: InstantDB.init(appId: 'app-123') Device2: InstantDB.init(appId: 'app-456') // Wrong! // ✅ Solution: Same app ID everywhere Device1: InstantDB.init(appId: 'your-app-id') Device2: InstantDB.init(appId: 'your-app-id') // ❌ Problem: Sync disabled InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: false, // This disables sync! ), ) // ✅ Solution: Enable sync InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, // Enable sync ), ) ``` #### 3. "Sync is slow or inconsistent" **Diagnostic widget:** ```dart class SyncDiagnostics extends StatefulWidget { @override State createState() => _SyncDiagnosticsState(); } class _SyncDiagnosticsState extends State { final List _syncEvents = []; StreamSubscription? _connectionSubscription; @override void initState() { super.initState(); _monitorSync(); } void _monitorSync() { final db = InstantProvider.of(context); // Monitor connection status changes _connectionSubscription = db.syncEngine?.connectionStatus.stream.listen((isConnected) { final event = '${DateTime.now()}: Connection ${isConnected ? 'restored' : 'lost'}'; setState(() { _syncEvents.add(event); if (_syncEvents.length > 50) { _syncEvents.removeRange(0, _syncEvents.length - 50); } }); }); } @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Scaffold( appBar: AppBar(title: const Text('Sync Diagnostics')), body: Column( children: [ // Current status Watch((context) { final isConnected = db.syncEngine?.connectionStatus.value ?? false; return Container( width: double.infinity, padding: const EdgeInsets.all(16), color: isConnected ? Colors.green : Colors.red, child: Text( 'Status: ${isConnected ? 'Connected' : 'Disconnected'}', style: const TextStyle(color: Colors.white, fontSize: 18), ), ); }), // Test sync button Padding( padding: const EdgeInsets.all(16), child: ElevatedButton( onPressed: _testSync, child: const Text('Test Sync'), ), ), // Event log Expanded( child: ListView.builder( itemCount: _syncEvents.length, itemBuilder: (context, index) { return ListTile( dense: true, title: Text( _syncEvents[index], style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), ), ); }, ), ), ], ), ); } Future _testSync() async { final db = InstantProvider.of(context); final testId = db.id(); setState(() { _syncEvents.add('${DateTime.now()}: Testing sync with item $testId'); }); try { await db.transact([ ...db.create('sync_test', { 'id': testId, 'timestamp': DateTime.now().millisecondsSinceEpoch, 'test': true, }), ]); setState(() { _syncEvents.add('${DateTime.now()}: Sync test transaction completed'); }); } catch (e) { setState(() { _syncEvents.add('${DateTime.now()}: Sync test failed: $e'); }); } } @override void dispose() { _connectionSubscription?.cancel(); super.dispose(); } } ``` ### Authentication Issues #### 1. "Invalid email format" error **Problem:** Email validation is too strict or has issues ```dart // Debug email validation void debugEmailValidation(String email) { final isValid = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$').hasMatch(email); print('Email: $email'); print('Valid: $isValid'); print('Length: ${email.length}'); if (!isValid) { if (email.contains(' ')) print('Contains spaces'); if (!email.contains('@')) print('Missing @ symbol'); if (!email.contains('.')) print('Missing domain extension'); } } ``` #### 2. Magic code/link not working **Debugging authentication flow:** ```dart class AuthDebugScreen extends StatefulWidget { @override State createState() => _AuthDebugScreenState(); } class _AuthDebugScreenState extends State { final _emailController = TextEditingController(); final _codeController = TextEditingController(); final List _debugLog = []; void _log(String message) { setState(() { _debugLog.add('${DateTime.now()}: $message'); }); print(message); } Future _testMagicCode() async { final email = _emailController.text.trim(); _log('Testing magic code for: $email'); try { final db = InstantProvider.of(context); // Step 1: Send magic code _log('Sending magic code...'); await db.auth.sendMagicCode(email); _log('Magic code sent successfully'); } catch (e) { _log('Failed to send magic code: $e'); if (e is InstantException) { _log('Error code: ${e.code}'); _log('Error message: ${e.message}'); // Specific debugging for common errors if (e.code == 'invalid_email') { _log('Email validation failed - check email format'); } else if (e.code == 'endpoint_not_found') { _log('Auth endpoint not found - check app configuration'); } } } } Future _testVerifyCode() async { final email = _emailController.text.trim(); final code = _codeController.text.trim(); _log('Verifying code: $code for email: $email'); try { final db = InstantProvider.of(context); final user = await db.auth.verifyMagicCode( email: email, code: code, ); _log('Verification successful!'); _log('User ID: ${user.id}'); _log('User email: ${user.email}'); } catch (e) { _log('Verification failed: $e'); if (e is InstantException) { _log('Error code: ${e.code}'); if (e.code == 'invalid_code') { _log('Code is invalid or expired'); } } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Auth Debug')), body: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), ), const SizedBox(height: 16), ElevatedButton( onPressed: _testMagicCode, child: const Text('Test Send Magic Code'), ), const SizedBox(height: 16), TextField( controller: _codeController, decoration: const InputDecoration(labelText: 'Magic Code'), ), const SizedBox(height: 16), ElevatedButton( onPressed: _testVerifyCode, child: const Text('Test Verify Code'), ), const SizedBox(height: 24), // Debug log Expanded( child: Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), ), child: ListView.builder( itemCount: _debugLog.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Text( _debugLog[index], style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), ), ); }, ), ), ), ], ), ), ); } } ``` ### Query Issues #### 1. "Query returns no results" **Debug query structure:** ```dart class QueryDebugger { static void debugQuery(InstantDB db, Map query) { print('=== Query Debug ==='); print('Query: ${jsonEncode(query)}'); // Test the query db.queryOnce(query).then((result) { print('Result: ${result.data}'); print('Error: ${result.error}'); if (result.data != null) { result.data!.forEach((entityType, entities) { final count = (entities as List).length; print('$entityType: $count items'); }); } }).catchError((error) { print('Query failed: $error'); }); } static void debugEntityStructure(InstantDB db, String entityType) { print('=== Entity Structure Debug ==='); // Get a sample of entities to see structure db.queryOnce({ entityType: {'limit': 5} }).then((result) { final entities = result.data?[entityType] as List? ?? []; if (entities.isEmpty) { print('No $entityType entities found'); return; } print('Sample $entityType entities:'); for (int i = 0; i < entities.length; i++) { final entity = entities[i] as Map; print('Entity $i: ${entity.keys.join(', ')}'); if (i == 0) { // Show full structure of first entity entity.forEach((key, value) { print(' $key: ${value.runtimeType} = $value'); }); } } }); } } // Usage void testQuery() { final query = { 'todos': { 'where': {'completed': false}, 'orderBy': {'createdAt': 'desc'}, } }; QueryDebugger.debugQuery(db, query); QueryDebugger.debugEntityStructure(db, 'todos'); } ``` #### 2. "Widget not updating when data changes" **Debug reactive updates:** ```dart class ReactivityDebugger extends StatefulWidget { final Map query; const ReactivityDebugger({super.key, required this.query}); @override State createState() => _ReactivityDebuggerState(); } class _ReactivityDebuggerState extends State { final List _updateLog = []; late Signal _querySignal; @override void initState() { super.initState(); final db = InstantProvider.of(context); _querySignal = db.subscribeQuery(widget.query); // Monitor signal changes _querySignal.toStream().listen((result) { final timestamp = DateTime.now().toString(); setState(() { _updateLog.add('$timestamp: Query updated'); if (_updateLog.length > 20) { _updateLog.removeRange(0, _updateLog.length - 20); } }); }); } @override Widget build(BuildContext context) { return Column( children: [ // Current data Watch((context) { final result = _querySignal.value; final hasData = result.data != null; final hasError = result.error != null; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: hasError ? Colors.red[100] : hasData ? Colors.green[100] : Colors.grey[100], border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Status: ${hasError ? 'Error' : hasData ? 'Data' : 'Loading'}'), if (hasError) Text('Error: ${result.error}'), if (hasData) Text('Data keys: ${result.data!.keys.join(', ')}'), ], ), ); }), const SizedBox(height: 16), // Update log Text('Update Log (${_updateLog.length}):'), Expanded( child: ListView.builder( itemCount: _updateLog.length, itemBuilder: (context, index) { return Text( _updateLog[index], style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), ); }, ), ), ], ); } } ``` ## Diagnostic Tools ### Logging Configuration Set up comprehensive logging: ```dart import 'package:logging/logging.dart'; void setupLogging() { // Set up hierarchical logging Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { final timestamp = record.time.toString().substring(11, 23); final level = record.level.name.padRight(7); final logger = record.loggerName.padRight(20); print('$timestamp $level $logger ${record.message}'); if (record.error != null) { print(' Error: ${record.error}'); } if (record.stackTrace != null) { print(' Stack: ${record.stackTrace}'); } }); } // Custom logger for InstantDB components class InstantDBLogger { static final _logger = Logger('InstantDB'); static void debug(String message) => _logger.fine(message); static void info(String message) => _logger.info(message); static void warning(String message) => _logger.warning(message); static void error(String message, [Object? error, StackTrace? stackTrace]) { _logger.severe(message, error, stackTrace); } } ``` ### Database Inspector Create a database inspection tool: ```dart class DatabaseInspector extends StatefulWidget { @override State createState() => _DatabaseInspectorState(); } class _DatabaseInspectorState extends State { String? _selectedEntity; Map? _entityData; @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Scaffold( appBar: AppBar( title: const Text('Database Inspector'), actions: [ IconButton( onPressed: _exportData, icon: const Icon(Icons.download), ), ], ), body: Row( children: [ // Entity list SizedBox( width: 200, child: _buildEntityList(db), ), const VerticalDivider(), // Entity data Expanded( child: _selectedEntity != null ? _buildEntityData(db, _selectedEntity!) : const Center(child: Text('Select an entity')), ), ], ), ); } Widget _buildEntityList(InstantDB db) { // List of known entity types - you might want to make this configurable final entityTypes = ['users', 'posts', 'comments', 'todos', 'sync_test']; return ListView.builder( itemCount: entityTypes.length, itemBuilder: (context, index) { final entityType = entityTypes[index]; return ListTile( title: Text(entityType), selected: _selectedEntity == entityType, onTap: () => _selectEntity(entityType), ); }, ); } Widget _buildEntityData(InstantDB db, String entityType) { return InstantBuilder( query: {entityType: {'limit': 100}}, builder: (context, result) { if (result.error != null) { return Center(child: Text('Error: ${result.error}')); } final entities = (result.data?[entityType] as List? ?? []) .cast>(); return Column( children: [ // Header Container( padding: const EdgeInsets.all(16), child: Row( children: [ Text( '$entityType (${entities.length} items)', style: Theme.of(context).textTheme.headlineSmall, ), const Spacer(), ElevatedButton( onPressed: () => _addTestEntity(db, entityType), child: const Text('Add Test Item'), ), ], ), ), // Data table Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SingleChildScrollView( child: _buildDataTable(entities), ), ), ), ], ); }, ); } Widget _buildDataTable(List> entities) { if (entities.isEmpty) { return const Center(child: Text('No data')); } // Get all unique keys final Set allKeys = {}; for (final entity in entities) { allKeys.addAll(entity.keys); } final keys = allKeys.toList()..sort(); return DataTable( columns: keys.map((key) => DataColumn(label: Text(key))).toList(), rows: entities.map((entity) { return DataRow( cells: keys.map((key) { final value = entity[key]; return DataCell( Text( value?.toString() ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, ), ); }).toList(), ); }).toList(), ); } void _selectEntity(String entityType) { setState(() { _selectedEntity = entityType; }); } Future _addTestEntity(InstantDB db, String entityType) async { final testData = { 'id': db.id(), 'test': true, 'createdAt': DateTime.now().millisecondsSinceEpoch, 'name': 'Test ${entityType.substring(0, entityType.length - 1)}', }; try { await db.transact([ ...db.create(entityType, testData), ]); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Test $entityType created')), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to create test $entityType: $e')), ); } } void _exportData() { // Export database data for debugging print('Database export requested'); // Implementation depends on your export requirements } } ``` ## Performance Issues ### Memory Leaks Detect and fix memory leaks: ```dart class MemoryLeakDetector { static final Map _widgetCounts = {}; static void registerWidget(String widgetName) { _widgetCounts[widgetName] = (_widgetCounts[widgetName] ?? 0) + 1; } static void unregisterWidget(String widgetName) { _widgetCounts[widgetName] = (_widgetCounts[widgetName] ?? 1) - 1; if (_widgetCounts[widgetName]! <= 0) { _widgetCounts.remove(widgetName); } } static void printReport() { print('=== Widget Memory Report ==='); _widgetCounts.forEach((widget, count) { if (count > 10) { // Threshold for potential leaks print('WARNING: $widget has $count instances (potential leak)'); } else { print('$widget: $count instances'); } }); } } // Use in your widgets class LeakAwareWidget extends StatefulWidget { @override State createState() => _LeakAwareWidgetState(); } class _LeakAwareWidgetState extends State { @override void initState() { super.initState(); MemoryLeakDetector.registerWidget('LeakAwareWidget'); } @override void dispose() { MemoryLeakDetector.unregisterWidget('LeakAwareWidget'); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } } ``` ## Error Recovery ### Automatic Error Recovery Implement robust error recovery: ```dart class ErrorRecoveryService { final InstantDB db; int _retryCount = 0; static const int _maxRetries = 3; static const Duration _retryDelay = Duration(seconds: 2); ErrorRecoveryService(this.db); Future withRetry( String operation, Future Function() action, ) async { _retryCount = 0; while (_retryCount < _maxRetries) { try { final result = await action(); _retryCount = 0; // Reset on success return result; } catch (e) { _retryCount++; print('$operation failed (attempt $_retryCount/$_maxRetries): $e'); if (_retryCount >= _maxRetries) { print('$operation failed permanently after $_maxRetries attempts'); rethrow; } // Wait before retry await Future.delayed(_retryDelay * _retryCount); // Try to recover based on error type await _attemptRecovery(e); } } throw Exception('Maximum retries exceeded for $operation'); } Future _attemptRecovery(dynamic error) async { if (error is InstantException) { switch (error.code) { case 'network_error': // Try to reconnect try { await db.syncEngine?.connect(); } catch (_) {} break; case 'auth_error': // Try to refresh auth try { await db.auth.refreshUser(); } catch (_) {} break; } } } } ``` ## Best Practices for Debugging ### 1. Enable Verbose Logging in Development ```dart InstantDB.init( appId: 'your-app-id', config: InstantConfig( syncEnabled: true, verboseLogging: kDebugMode, // Only in debug mode ), ); ``` ### 2. Use Structured Error Handling ```dart Future handleOperation(Future Function() operation) async { try { await operation(); } on InstantException catch (e) { // Handle InstantDB specific errors print('InstantDB Error: ${e.code} - ${e.message}'); _showUserFriendlyError(e); } catch (e, stackTrace) { // Handle other errors print('Unexpected error: $e'); print('Stack trace: $stackTrace'); _reportError(e, stackTrace); } } ``` ### 3. Create Debug Screens ```dart class DebugScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Debug Tools')), body: ListView( children: [ ListTile( title: const Text('Sync Diagnostics'), onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => SyncDiagnostics()), ), ), ListTile( title: const Text('Database Inspector'), onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => DatabaseInspector()), ), ), ListTile( title: const Text('Auth Debug'), onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => AuthDebugScreen()), ), ), ], ), ); } } ``` ### 4. Monitor App State ```dart class AppStateMonitor extends StatefulWidget { final Widget child; const AppStateMonitor({super.key, required this.child}); @override State createState() => _AppStateMonitorState(); } class _AppStateMonitorState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { print('App lifecycle state changed: $state'); final db = InstantProvider.of(context); switch (state) { case AppLifecycleState.paused: // App is paused - might want to pause sync break; case AppLifecycleState.resumed: // App is resumed - ensure connection is active db.syncEngine?.connect(); break; case AppLifecycleState.detached: // App is being terminated break; default: break; } } @override Widget build(BuildContext context) { return widget.child; } } ``` ## Getting Help ### 1. Collect Debug Information Before asking for help, collect this information: ```dart void collectDebugInfo() { print('=== InstantDB Debug Info ==='); print('App ID: ${db.appId}'); print('Sync enabled: ${db.config.syncEnabled}'); print('Is authenticated: ${db.auth.isAuthenticated}'); print('Current user: ${db.auth.currentUser.value?.email}'); print('Connection status: ${db.syncEngine?.connectionStatus.value}'); print('Flutter version: ${Platform.version}'); print('Platform: ${Platform.operatingSystem}'); } ``` ### 2. Reproduce Issues Create minimal reproduction cases: ```dart Future reproduceIssue() async { // Minimal code that reproduces the problem final db = await InstantDB.init(appId: 'your-app-id'); try { // Steps to reproduce await db.transact([...db.create('test', {'id': db.id()})]); } catch (e) { print('Issue reproduced: $e'); } } ``` ### 3. Report Bugs Include this information when reporting bugs: - Flutter InstantDB version - Flutter/Dart version - Platform (iOS/Android/Web) - Complete error messages and stack traces - Steps to reproduce - Expected vs actual behavior ## Next Steps Learn more about maintaining robust InstantDB applications: - [Migration Strategies](/docs/advanced/migration) - Handling app updates and migrations - [Performance Optimization](/docs/advanced/performance) - Preventing performance issues - [Offline Functionality](/docs/advanced/offline) - Debugging offline scenarios - [API Reference](/docs/api) - Complete API documentation --- # InstantDB API > Complete API reference for the core InstantDB class Source: https://flutter-instantdb.vercel.app/docs/api/instantdb The `InstantDB` class is the main entry point for interacting with your InstantDB database. It provides methods for initialization, queries, transactions, authentication, and real-time synchronization. ## Initialization ### `InstantDB.init()` Initialize a new InstantDB instance. ```dart static Future init({ required String appId, InstantConfig config = const InstantConfig(), }) ``` **Parameters:** - `appId` (`String`): Your InstantDB application ID - `config` (`InstantConfig`, optional): Configuration options **Returns:** `Future` - The initialized database instance **Example:** ```dart final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, verboseLogging: false, ), ); ``` ### `InstantConfig` Configuration options for InstantDB initialization. ```dart class InstantConfig { const InstantConfig({ this.persistenceDir, this.syncEnabled = true, this.baseUrl = 'https://api.instantdb.com', this.maxCacheSize = 50 * 1024 * 1024, // 50MB this.reconnectDelay = const Duration(seconds: 1), this.verboseLogging = false, }); final String? persistenceDir; final bool syncEnabled; final String baseUrl; final int maxCacheSize; final Duration reconnectDelay; final bool verboseLogging; } ``` **Properties:** - `persistenceDir` (`String?`): Custom directory for SQLite database storage - `syncEnabled` (`bool`): Enable real-time synchronization (default: `true`) - `baseUrl` (`String`): Custom API endpoint URL (default: `https://api.instantdb.com`) - `maxCacheSize` (`int`): Maximum size of the local database cache in bytes (default: 50MB) - `reconnectDelay` (`Duration`): Delay between reconnection attempts (default: 1 second) - `verboseLogging` (`bool`): Enable detailed debug logging (default: `false`) ## Core Properties ### `isReady` A reactive signal indicating if the database is initialized and ready for use. ```dart ReadonlySignal get isReady ``` **Example:** ```dart if (db.isReady.value) { print('Database is ready'); } ``` ### `isOnline` A reactive signal indicating if the database is currently connected to the sync server. ```dart ReadonlySignal get isOnline ``` **Example:** ```dart Watch((context) { final online = db.isOnline.value; return Text(online ? 'Connected' : 'Disconnected'); }); ``` ### `appId` Get the application ID for this database instance. ```dart String get appId ``` **Returns:** `String` - The application ID **Example:** ```dart print('Database app ID: ${db.appId}'); ``` ### `auth` Access the authentication manager. ```dart AuthManager get auth ``` **Returns:** `AuthManager` - The authentication manager instance **Example:** ```dart final currentUser = db.auth.currentUser.value; if (currentUser != null) { print('User: ${currentUser.email}'); } ``` ### `presence` Access the presence manager for real-time collaboration. ```dart PresenceManager get presence ``` **Returns:** `PresenceManager` - The presence manager instance **Example:** ```dart final room = db.presence.joinRoom('chat-room'); await room.setPresence({'status': 'online'}); ``` ### `syncEngine` Access the internal synchronization engine. Note that most synchronization logic is handled automatically through `db.query` and `db.transact`. ```dart SyncEngine get syncEngine ``` **Returns:** `SyncEngine` - The sync engine instance ## Query Methods ### `query()` Create a reactive query that updates automatically when data changes. This is the preferred method for getting data. ```dart Signal query(Map query, {bool syncedOnly = false}) ``` **Parameters:** - `query` (`Map`): The InstaQL query object - `syncedOnly` (`bool`, optional): If true, only returns entities that sync to cloud (excludes local-only entities) **Returns:** `Signal` - A reactive signal containing query results **Example:** ```dart final todosSignal = db.query({ 'todos': { 'where': {'completed': false}, 'orderBy': {'createdAt': 'desc'}, 'limit': 20, }, }); ``` ### `subscribeQuery()` Alias for `query()`, kept for API compatibility. ```dart Signal subscribeQuery(Map query) ``` **Example:** ```dart final todosSignal = db.subscribeQuery({ 'todos': { 'where': {'completed': false}, 'orderBy': {'createdAt': 'desc'}, 'limit': 20, }, }); // Use in reactive widgets Watch((context) { final result = todosSignal.value; final todos = result.data?['todos'] ?? []; return TodoList(todos: todos); }); ``` ### `queryOnce()` Execute a one-time query without creating a subscription. ```dart Future queryOnce(Map query, {bool syncedOnly = false}) ``` **Parameters:** - `query` (`Map`): The InstaQL query object - `syncedOnly` (`bool`, optional): If true, only returns entities that sync to cloud **Returns:** `Future` - The query result **Example:** ```dart final result = await db.queryOnce({ 'users': { 'where': {'role': 'admin'}, 'limit': 5, }, }); final adminUsers = result.data?['users'] ?? []; print('Found ${adminUsers.length} admin users'); ``` ## Transaction Methods ### `transact()` Execute a transaction with a list of operations or transaction chunk. ```dart Future transact(dynamic transaction) ``` **Parameters:** - `transaction` (`List` or `TransactionChunk`): The operations to execute **Returns:** `Future` - The transaction result **Example:** ```dart // With list of operations await db.transact([ ...db.create('todos', { 'id': db.id(), 'text': 'Learn InstantDB', 'completed': false, }), ]); // With transaction chunk (new tx API) await db.transact( db.tx['todos'][todoId].update({'completed': true}) ); ``` ### `transactChunk()` (Deprecated) Execute a transaction chunk. Use `transact()` instead. ```dart @Deprecated('Use transact() instead') Future transactChunk(TransactionChunk chunk) ``` ### `create()` Create a new entity operation. ```dart List create(String entityType, Map data) ``` **Parameters:** - `entityType` (`String`): The type of entity to create - `data` (`Map`): The entity data **Returns:** `List` - List containing the create operation **Example:** ```dart final operations = db.create('posts', { 'id': db.id(), 'title': 'Hello World', 'content': 'This is my first post', 'authorId': userId, 'createdAt': DateTime.now().millisecondsSinceEpoch, }); await db.transact(operations); ``` ### `update()` Create an update entity operation. ```dart Operation update(String entityId, Map data) ``` **Parameters:** - `entityId` (`String`): The ID of the entity to update - `data` (`Map`): The data to update **Returns:** `Operation` - The update operation **Example:** ```dart await db.transact([ db.update(postId, { 'title': 'Updated Title', 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); ``` ### `delete()` Create a delete entity operation. ```dart Operation delete(String entityId) ``` **Parameters:** - `entityId` (`String`): The ID of the entity to delete **Returns:** `Operation` - The delete operation **Example:** ```dart await db.transact([ db.delete(postId), ]); ``` ### `merge()` Create a deep merge operation. ```dart Operation merge(String entityId, Map data) ``` **Parameters:** - `entityId` (`String`): The ID of the entity to merge - `data` (`Map`): The data to deep merge **Returns:** `Operation` - The merge operation **Example:** ```dart await db.transact([ db.merge(userId, { 'preferences': { 'theme': 'dark', 'notifications': {'email': false}, }, }), ]); ``` ### `link()` Create a link operation between entities. ```dart Operation link(String fromId, String linkName, String toId) ``` **Parameters:** - `fromId` (`String`): The source entity ID - `linkName` (`String`): The name of the link relationship - `toId` (`String`): The target entity ID **Returns:** `Operation` - The link operation **Example:** ```dart await db.transact([ db.link(userId, 'posts', postId), ]); ``` ### `unlink()` Create an unlink operation between entities. ```dart Operation unlink(String fromId, String linkName, String toId) ``` **Parameters:** - `fromId` (`String`): The source entity ID - `linkName` (`String`): The name of the link relationship - `toId` (`String`): The target entity ID **Returns:** `Operation` - The unlink operation **Example:** ```dart await db.transact([ db.unlink(userId, 'posts', postId), ]); ``` ## Transaction API (tx namespace) ### `tx` Access the new transaction API namespace for fluent operations. ```dart TransactionNamespace get tx ``` **Returns:** `TransactionNamespace` - The transaction namespace **Example:** ```dart // Fluent API for complex operations await db.transact( db.tx['users'][userId] .update({'name': 'New Name'}) .link({'posts': [postId]}) .merge({ 'preferences': {'theme': 'dark'}, }) ); ``` ## Utility Methods ### `id()` Generate a new UUID for entity IDs. ```dart String id() ``` **Returns:** `String` - A new UUID string **Example:** ```dart final newId = db.id(); print('Generated ID: $newId'); // e.g., "123e4567-e89b-12d3-a456-426614174000" await db.transact([ ...db.create('items', { 'id': newId, 'name': 'New Item', }), ]); ``` ### `lookup()` Create a lookup reference for referencing entities by attribute instead of ID. ```dart LookupRef lookup(String entityType, String attribute, dynamic value) ``` **Parameters:** - `entityType` (`String`): The type of entity to lookup - `attribute` (`String`): The attribute to match against - `value` (`dynamic`): The value to match **Returns:** `LookupRef` - A lookup reference object **Example:** ```dart // Reference user by email instead of ID await db.transact([ ...db.create('posts', { 'id': db.id(), 'title': 'Hello World', 'authorId': lookup('users', 'email', 'john@example.com'), }), ]); // Use in queries final posts = db.subscribeQuery({ 'posts': { 'where': { 'author': lookup('users', 'email', 'john@example.com'), }, }, }); ``` ## Authentication Helpers ### `getAuth()` Get the current authentication state (one-time check). ```dart AuthUser? getAuth() ``` **Returns:** `AuthUser?` - The current user, or null if not authenticated **Example:** ```dart final currentUser = db.getAuth(); if (currentUser != null) { print('Logged in as: ${currentUser.email}'); } else { print('Not logged in'); } ``` ### `subscribeAuth()` Get a stream of authentication state changes. ```dart Stream subscribeAuth() ``` **Returns:** `Stream` - A stream of auth user state **Example:** ```dart db.subscribeAuth().listen((user) { if (user != null) { print('User signed in: ${user.email}'); } else { print('User signed out'); } }); ``` ### `getAnonymousUserId()` Get or generate an anonymous user ID for presence and collaboration. ```dart String getAnonymousUserId() ``` **Returns:** `String` - An anonymous user ID **Example:** ```dart final anonymousId = db.getAnonymousUserId(); final room = db.presence.joinRoom('public-room', initialPresence: { 'userId': anonymousId, 'userName': 'Guest ${anonymousId.substring(0, 4)}', }); ``` ## Lifecycle Methods ### `dispose()` Clean up database resources and connections. ```dart Future dispose() ``` **Returns:** `Future` **Example:** ```dart @override void dispose() { super.dispose(); // Clean up database when app is disposed db.dispose(); } ``` ## Error Handling All InstantDB methods can throw `InstantException` for database-related errors: ```dart try { await db.transact([ ...db.create('invalid', {}), // Missing required fields ]); } on InstantException catch (e) { print('InstantDB Error: ${e.code}'); print('Message: ${e.message}'); // Handle specific error types switch (e.code) { case 'validation_error': // Handle validation errors break; case 'network_error': // Handle network issues break; case 'auth_error': // Handle authentication errors break; } } catch (e) { print('Unexpected error: $e'); } ``` ## Complete Example Here's a complete example showing common InstantDB operations: ```dart class TodoService { final InstantDB db; TodoService(this.db); // Get reactive todos Signal getTodos({bool? completed}) { return db.subscribeQuery({ 'todos': { if (completed != null) 'where': {'completed': completed}, 'orderBy': {'createdAt': 'desc'}, }, }); } // Create a new todo Future createTodo({ required String text, bool completed = false, }) async { await db.transact([ ...db.create('todos', { 'id': db.id(), 'text': text, 'completed': completed, 'createdAt': DateTime.now().millisecondsSinceEpoch, 'userId': db.auth.currentUser.value?.id, }), ]); } // Update todo using new tx API Future updateTodo(String todoId, {String? text, bool? completed}) async { final updates = {}; if (text != null) updates['text'] = text; if (completed != null) updates['completed'] = completed; updates['updatedAt'] = DateTime.now().millisecondsSinceEpoch; await db.transact( db.tx['todos'][todoId].update(updates) ); } // Delete todo Future deleteTodo(String todoId) async { await db.transact([db.delete(todoId)]); } // Get todos by user email (using lookup) Signal getTodosByUser(String email) { return db.subscribeQuery({ 'todos': { 'where': { 'user': lookup('users', 'email', email), }, }, }); } // Bulk operations Future markAllCompleted() async { final result = await db.queryOnce({ 'todos': {'where': {'completed': false}}, }); final todos = (result.data?['todos'] as List? ?? []) .cast>(); final operations = todos.map((todo) => db.update(todo['id'], {'completed': true}) ).toList(); if (operations.isNotEmpty) { await db.transact(operations); } } } ``` ## Next Steps Explore specific API areas: - [Transactions API](/docs/api/transactions) - Detailed transaction and operation methods - [Queries API](/docs/api/queries) - Advanced querying and InstaQL syntax - [Presence API](/docs/api/presence-api) - Real-time collaboration methods - [Flutter Widgets](/docs/api/widgets) - Reactive UI components - [Types Reference](/docs/api/types) - Complete type definitions --- # Presence API > Complete API reference for InstantDB presence and real-time collaboration Source: https://flutter-instantdb.vercel.app/docs/api/presence-api The Presence API enables real-time collaboration features like cursors, typing indicators, reactions, and user awareness. It provides both room-based and direct APIs for building collaborative applications. ## PresenceManager The main entry point for presence functionality, accessed via `db.presence`. ### `joinRoom()` Join a presence room and get a scoped API for room operations. ```dart InstantRoom joinRoom( String roomId, { Map? initialPresence, }) ``` **Parameters:** - `roomId` (`String`): Unique identifier for the room - `initialPresence` (`Map?`): Initial presence data **Returns:** `InstantRoom` - Room instance with scoped operations **Example:** ```dart final room = db.presence.joinRoom('editor-room', initialPresence: { 'userName': 'Alice', 'status': 'online', 'color': '#ff6b6b', }); ``` ### `leaveRoom()` Leave a presence room and clean up resources. ```dart Future leaveRoom(String roomId) ``` **Parameters:** - `roomId` (`String`): Room ID to leave **Example:** ```dart await db.presence.leaveRoom('editor-room'); ``` ### Direct Presence Methods For simple use cases, you can use direct methods without joining a room: #### `setPresence()` ```dart Future setPresence(String roomId, Map presence) ``` #### `updateCursor()` ```dart Future updateCursor(String roomId, {required double x, required double y}) ``` #### `setTyping()` ```dart Future setTyping(String roomId, bool isTyping) ``` #### `sendReaction()` ```dart Future sendReaction(String roomId, String reaction, {Map? metadata}) ``` ## InstantRoom Room-scoped presence API providing isolated operations for a specific collaboration space. ### Presence Operations #### `setPresence()` Update user presence data in the room. ```dart Future setPresence(Map presence) ``` **Parameters:** - `presence` (`Map`): Presence data to set **Example:** ```dart await room.setPresence({ 'status': 'editing', 'currentDocument': documentId, 'mood': '😊', 'tool': 'text-editor', }); ``` #### `getPresence()` Get a reactive signal of all user presence data in the room. ```dart Signal> getPresence() ``` **Returns:** `Signal>` - Map of user ID to presence data **Example:** ```dart Watch((context) { final presence = room.getPresence().value; final onlineCount = presence.values.length; return Text('$onlineCount users in room'); }); ``` ### Cursor Operations #### `updateCursor()` Update cursor position in the room. ```dart Future updateCursor({required double x, required double y}) ``` **Parameters:** - `x` (`double`): X coordinate - `y` (`double`): Y coordinate **Example:** ```dart void _onMouseMove(PointerEvent event) { room.updateCursor( x: event.localPosition.dx, y: event.localPosition.dy, ); } ``` #### `getCursors()` Get a reactive signal of all cursor positions in the room. ```dart Signal> getCursors() ``` **Returns:** `Signal>` - Map of user ID to cursor data **Example:** ```dart Watch((context) { final cursors = room.getCursors().value; return Stack( children: cursors.entries.map((entry) { final cursor = entry.value; return Positioned( left: cursor.x, top: cursor.y, child: CursorWidget( userName: cursor.userName ?? 'Unknown', userColor: cursor.userColor != null ? Color(int.parse(cursor.userColor!.replaceAll('#', '0xFF'))) : Colors.blue, ), ); }).toList(), ); }); ``` ### Typing Operations #### `setTyping()` Set typing indicator status. ```dart Future setTyping(bool isTyping) ``` **Parameters:** - `isTyping` (`bool`): Whether user is currently typing **Example:** ```dart final _textController = TextEditingController(); Timer? _typingTimer; void _onTextChanged(String text) { // Set typing to true room.setTyping(true); // Clear typing after delay _typingTimer?.cancel(); _typingTimer = Timer(const Duration(seconds: 2), () { room.setTyping(false); }); } ``` #### `getTyping()` Get a reactive signal of users currently typing. Returns a map of user IDs to the timestamp they were last seen typing. ```dart Signal> getTyping() ``` **Returns:** `Signal>` - Map of user IDs to typing timestamps **Example:** ```dart Watch((context) { final typing = room.getTyping().value; if (typing.isEmpty) { return const SizedBox.shrink(); } return Text('${typing.length} people are typing...'); }); ``` ### Reaction Operations #### `sendReaction()` Send a reaction to the room. ```dart Future sendReaction( String reaction, { Map? metadata, }) ``` **Parameters:** - `reaction` (`String`): Reaction emoji or identifier - `metadata` (`Map?`): Additional reaction data **Example:** ```dart void _onDoubleTap(TapDownDetails details) { room.sendReaction('❤️', metadata: { 'x': details.localPosition.dx, 'y': details.localPosition.dy, 'message': 'Great idea!', 'timestamp': DateTime.now().millisecondsSinceEpoch, }); } ``` #### `getReactions()` Get a reactive signal of recent reactions. ```dart Signal> getReactions() ``` **Returns:** `Signal>` - List of recent reactions **Example:** ```dart Watch((context) { final reactions = room.getReactions().value; return Stack( children: reactions.map((reaction) { final metadata = reaction.metadata; final x = metadata?['x']?.toDouble() ?? 0.0; final y = metadata?['y']?.toDouble() ?? 0.0; return Positioned( left: x, top: y, child: AnimatedReaction( emoji: reaction.emoji, onComplete: () { // Reaction animation completed }, ), ); }).toList(), ); }); ``` ## Topic-Based Messaging Rooms support topic-based messaging for structured communication. ### `publishTopic()` Publish a message to a specific topic within the room. ```dart Future publishTopic(String topic, Map data) ``` **Parameters:** - `topic` (`String`): Topic name - `data` (`Map`): Message data **Example:** ```dart await room.publishTopic('chat', { 'message': 'Hello everyone!', 'userName': 'Alice', 'timestamp': DateTime.now().millisecondsSinceEpoch, }); await room.publishTopic('document-changes', { 'type': 'text-insert', 'position': 42, 'text': 'Hello', 'userId': currentUserId, }); ``` ### `subscribeTopic()` Subscribe to messages on a specific topic. ```dart Stream> subscribeTopic(String topic) ``` **Parameters:** - `topic` (`String`): Topic name to subscribe to **Returns:** `Stream>` - Stream of messages **Example:** ```dart class ChatRoomWidget extends StatefulWidget { final InstantRoom room; const ChatRoomWidget({super.key, required this.room}); @override State createState() => _ChatRoomWidgetState(); } class _ChatRoomWidgetState extends State { final List _messages = []; StreamSubscription? _chatSubscription; @override void initState() { super.initState(); // Subscribe to chat messages _chatSubscription = widget.room.subscribeTopic('chat').listen((data) { final message = ChatMessage.fromJson(data); setState(() { _messages.add(message); }); }); } @override void dispose() { _chatSubscription?.cancel(); super.dispose(); } void _sendMessage(String text) { widget.room.publishTopic('chat', { 'message': text, 'userName': 'Current User', 'timestamp': DateTime.now().millisecondsSinceEpoch, }); } @override Widget build(BuildContext context) { return Column( children: [ Expanded( child: ListView.builder( itemCount: _messages.length, itemBuilder: (context, index) { return MessageBubble(message: _messages[index]); }, ), ), MessageInput(onSend: _sendMessage), ], ); } } ``` ## Data Types ### `PresenceData` Represents a user's presence data. ```dart class PresenceData { final String userId; final Map data; final DateTime lastSeen; const PresenceData({ required this.userId, required this.data, required this.lastSeen, }); } ``` **Properties:** - `userId` (`String`): Unique user identifier - `data` (`Map`): Custom presence data - `lastSeen` (`DateTime`): When user was last seen in the room ### `CursorData` Represents cursor position and metadata. ```dart class CursorData { final String userId; final String? userName; final String? userColor; final double x; final double y; final Map? metadata; final DateTime lastUpdated; const CursorData({ required this.userId, this.userName, this.userColor, required this.x, required this.y, this.metadata, required this.lastUpdated, }); } ``` **Properties:** - `userId` (`String`): User who owns the cursor - `userName` (`String?`): Optional display name for the user - `userColor` (`String?`): Optional hex color string for the cursor - `x` (`double`): X coordinate - `y` (`double`): Y coordinate - `metadata` (`Map?`): Additional custom cursor metadata - `lastUpdated` (`DateTime`): When cursor was last updated ### `ReactionData` Represents a reaction sent to the room. ```dart class ReactionData { final String id; final String userId; final String emoji; final String? messageId; final Map? metadata; final DateTime timestamp; const ReactionData({ required this.id, required this.userId, required this.emoji, this.messageId, this.metadata, required this.timestamp, }); } ``` **Properties:** - `id` (`String`): Unique reaction identifier - `userId` (`String`): User who sent the reaction - `emoji` (`String`): Reaction emoji - `messageId` (`String?`): Optional ID of a message this reaction refers to - `metadata` (`Map?`): Custom reaction metadata - `timestamp` (`DateTime`): When reaction was created ## Complete Examples ### Collaborative Text Editor ```dart class CollaborativeEditor extends StatefulWidget { final String documentId; const CollaborativeEditor({super.key, required this.documentId}); @override State createState() => _CollaborativeEditorState(); } class _CollaborativeEditorState extends State { final TextEditingController _controller = TextEditingController(); InstantRoom? _room; Timer? _typingTimer; @override void initState() { super.initState(); _initializeCollaboration(); } void _initializeCollaboration() { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; _room = db.presence.joinRoom('doc-${widget.documentId}', initialPresence: { 'userName': currentUser?.email ?? 'Anonymous', 'status': 'editing', 'color': _generateUserColor(currentUser?.id ?? 'anonymous'), }); // Listen to text changes for typing indicators _controller.addListener(_handleTextChange); // Listen to selection changes for cursor updates _controller.addListener(_handleSelectionChange); } void _handleTextChange() { // Set typing status _room?.setTyping(true); // Clear typing after delay _typingTimer?.cancel(); _typingTimer = Timer(const Duration(seconds: 2), () { _room?.setTyping(false); }); } void _handleSelectionChange() { final selection = _controller.selection; if (selection.isValid) { // Convert text position to screen coordinates (simplified) final cursorX = selection.baseOffset * 10.0; // Approximation final cursorY = 0.0; _room?.updateCursor(x: cursorX, y: cursorY); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Collaborative Editor'), actions: [ // Show connected users if (_room != null) UserAvatars(room: _room!), ], ), body: Column( children: [ // Typing indicators if (_room != null) TypingIndicator(room: _room!), // Main editor Expanded( child: Stack( children: [ // Text input Padding( padding: const EdgeInsets.all(16), child: TextField( controller: _controller, maxLines: null, expands: true, decoration: const InputDecoration( border: InputBorder.none, hintText: 'Start typing...', ), ), ), // Cursor overlay if (_room != null) CursorOverlay(room: _room!), // Reactions overlay if (_room != null) ReactionsOverlay(room: _room!), ], ), ), ], ), ); } Color _generateUserColor(String userId) { final hash = userId.hashCode; return Color(0xFF000000 | (hash & 0xFFFFFF)); } @override void dispose() { _room?.setPresence({'status': 'offline'}); _controller.dispose(); _typingTimer?.cancel(); super.dispose(); } } ``` ### Multiplayer Whiteboard ```dart class CollaborativeWhiteboard extends StatefulWidget { @override State createState() => _CollaborativeWhiteboardState(); } class _CollaborativeWhiteboardState extends State { InstantRoom? _room; final List _points = []; StreamSubscription? _drawingSubscription; @override void initState() { super.initState(); _initializeWhiteboard(); } void _initializeWhiteboard() { final db = InstantProvider.of(context); _room = db.presence.joinRoom('whiteboard', initialPresence: { 'userName': 'Artist ${DateTime.now().millisecondsSinceEpoch % 1000}', 'tool': 'pen', 'color': '#000000', }); // Subscribe to drawing events _drawingSubscription = _room!.subscribeTopic('drawing').listen((data) { final point = DrawingPoint.fromJson(data); setState(() { _points.add(point); }); }); } void _handlePanStart(DragStartDetails details) { _room?.setPresence({'status': 'drawing'}); _addPoint(details.localPosition, isStart: true); } void _handlePanUpdate(DragUpdateDetails details) { _addPoint(details.localPosition, isStart: false); // Update cursor for other users _room?.updateCursor( x: details.localPosition.dx, y: details.localPosition.dy, ); } void _handlePanEnd(DragEndDetails details) { _room?.setPresence({'status': 'idle'}); } void _addPoint(Offset position, {required bool isStart}) { final point = DrawingPoint( x: position.dx, y: position.dy, isStart: isStart, userId: 'current-user', // Get from auth timestamp: DateTime.now().millisecondsSinceEpoch, ); // Broadcast drawing point _room?.publishTopic('drawing', point.toJson()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Collaborative Whiteboard'), actions: [ if (_room != null) UserAvatars(room: _room!), ], ), body: Stack( children: [ // Drawing canvas GestureDetector( onPanStart: _handlePanStart, onPanUpdate: _handlePanUpdate, onPanEnd: _handlePanEnd, child: CustomPaint( painter: WhiteboardPainter(_points), size: Size.infinite, ), ), // Collaborative cursors if (_room != null) CursorOverlay(room: _room!), // Reactions if (_room != null) GestureDetector( onDoubleTapDown: (details) { _room!.sendReaction('✨', metadata: { 'x': details.localPosition.dx, 'y': details.localPosition.dy, }); }, child: ReactionsOverlay(room: _room!), ), ], ), ); } @override void dispose() { _room?.setPresence({'status': 'offline'}); _drawingSubscription?.cancel(); super.dispose(); } } ``` ## Performance Considerations ### Throttle Updates Prevent excessive presence updates: ```dart class ThrottledPresence { final InstantRoom room; Timer? _cursorTimer; static const Duration _throttleDuration = Duration(milliseconds: 100); ThrottledPresence(this.room); void updateCursor(double x, double y) { _cursorTimer?.cancel(); _cursorTimer = Timer(_throttleDuration, () { room.updateCursor(x: x, y: y); }); } void dispose() { _cursorTimer?.cancel(); } } ``` ### Limit Displayed Elements Prevent performance issues with many users: ```dart Watch((context) { final cursors = room.getCursors().value; // Limit to 10 most recent cursors final recentCursors = cursors.entries .toList() ..sort((a, b) => b.value.lastUpdated.compareTo(a.value.lastUpdated)) ..take(10); return Stack( children: recentCursors.map((entry) => CursorWidget(cursor: entry.value) ).toList(), ); }); ``` ## Error Handling ```dart class SafePresenceOperations { final InstantRoom room; SafePresenceOperations(this.room); Future safeSetPresence(Map presence) async { try { await room.setPresence(presence); } catch (e) { print('Failed to set presence: $e'); // Handle gracefully - presence is not critical } } Future safeUpdateCursor(double x, double y) async { try { await room.updateCursor(x: x, y: y); } catch (e) { print('Failed to update cursor: $e'); // Cursor updates are not critical } } } ``` ## Next Steps Explore related APIs and features: - [InstantDB Core](/docs/api/instantdb) - Main database initialization and methods - [Flutter Widgets](/docs/api/widgets) - Presence-aware reactive widgets - [Real-time Sync](/docs/realtime/sync) - Understanding data synchronization - [Collaborative Features](/docs/realtime/collaboration) - Complete collaboration examples - [Types Reference](/docs/api/types) - Presence data types and structures --- # Queries API > Complete API reference for InstantDB queries and InstaQL syntax Source: https://flutter-instantdb.vercel.app/docs/api/queries InstantDB uses InstaQL, a declarative query language that provides powerful filtering, sorting, and relationship traversal capabilities. All queries are reactive by default and automatically update when underlying data changes. ## Query Methods ### `query()` Create a reactive query that automatically updates when data changes. ```dart Signal query(Map query, {bool syncedOnly = false}) ``` **Parameters:** - `query` (`Map`): InstaQL query object - `syncedOnly` (`bool`, optional): If true, only returns entities that are synced to the cloud. **Returns:** `Signal` - Reactive signal containing query results **Example:** ```dart final todosSignal = db.query({ 'todos': { 'where': {'completed': false}, 'orderBy': {'createdAt': 'desc'}, 'limit': 10, }, }); // Use in reactive widgets Watch((context) { final result = todosSignal.value; final todos = result.data?['todos'] ?? []; return TodoList(todos: todos); }); ``` ### `queryOnce()` Execute a one-time query without creating a subscription. ```dart Future queryOnce(Map query) ``` **Parameters:** - `query` (`Map`): InstaQL query object **Returns:** `Future` - Query result **Example:** ```dart final result = await db.queryOnce({ 'users': { 'where': {'role': 'admin'}, 'limit': 5, }, }); final adminUsers = result.data?['users'] ?? []; print('Found ${adminUsers.length} admin users'); ``` ## QueryResult Result object returned by query operations. ```dart class QueryResult { final Map? data; final String? error; final bool isLoading; } ``` **Properties:** - `data` (`Map?`): Query result data indexed by entity type - `error` (`String?`): Error message if query failed - `isLoading` (`bool`): Whether query is currently loading ## InstaQL Syntax ### Basic Query Structure ```dart { 'entityType': { 'where': {...}, // Filtering conditions 'orderBy': {...}, // Sorting specification 'limit': 10, // Maximum results 'offset': 20, // Skip results (pagination) 'include': {...}, // Include related entities } } ``` ### Simple Queries ```dart // Get all entities {'users': {}} // Get with limit {'posts': {'limit': 10}} // Get specific entity by ID {'users': {'where': {'id': 'user-123'}}} // Multiple entity types { 'users': {'limit': 5}, 'posts': {'where': {'published': true}}, } ``` ## Where Conditions ### Equality ```dart // Exact match {'users': {'where': {'name': 'John Doe'}}} // Multiple conditions (AND) { 'posts': { 'where': { 'published': true, 'authorId': 'user-123', } } } ``` ### Comparison Operators ```dart { 'posts': { 'where': { 'createdAt': { '\$gte': DateTime.now().subtract(Duration(days: 7)).millisecondsSinceEpoch, '\$lt': DateTime.now().millisecondsSinceEpoch, }, 'viewCount': {'\$gt': 100}, 'rating': {'\$lte': 5}, } } } ``` **Available operators:** - `$gt`: Greater than - `$gte`: Greater than or equal - `$lt`: Less than - `$lte`: Less than or equal - `$ne`: Not equal - `$eq`: Equal (explicit equality) ### String Operators ```dart { 'users': { 'where': { 'email': {'$like': '%@gmail.com'}, // Contains pattern (% matches any characters) 'name': {'$ilike': '%john%'}, // Case-insensitive like } } } ``` ### Array Operators ```dart { 'posts': { 'where': { 'tags': {'$contains': 'flutter'}, // Array contains value 'skills': {'$size': 3}, // Array matches exact size 'permissions': {'$in': ['read', 'write']}, // Value in list 'blockedUsers': {'$nin': ['user-123']}, // Value not in list } } } ``` ### Existence Operators ```dart { 'users': { 'where': { 'profilePicture': {'$exists': true}, // Field exists (is not null) 'deletedAt': {'$isNull': true}, // Field is null 'verifiedAt': {'$isNull': false}, // Field is not null (using $isNull: false) } } } ``` ### Logical Operators #### AND Operator ```dart // Implicit AND (default) { 'users': { 'where': { 'active': true, 'role': 'user', // Both conditions must be true } } } // Explicit AND { 'users': { 'where': { '\$and': [ {'age': {'\$gte': 18}}, {'status': 'verified'}, ] } } } ``` #### OR Operator ```dart { 'users': { 'where': { '\$or': [ {'role': 'admin'}, {'role': 'moderator'}, {'permissions': {'\$contains': 'admin'}}, ] } } } ``` #### NOT Operator ```dart { 'posts': { 'where': { '\$not': { 'status': 'deleted', } } } } ``` #### Complex Logic ```dart { 'users': { 'where': { '\$and': [ {'active': true}, { '\$or': [ {'role': 'admin'}, { '\$and': [ {'role': 'user'}, {'permissions': {'\$contains': 'write'}}, ] } ] } ] } } } ``` ## Sorting (orderBy) ### Single Field ```dart // Ascending order {'posts': {'orderBy': {'createdAt': 'asc'}}} // Descending order {'posts': {'orderBy': {'createdAt': 'desc'}}} ``` ### Multiple Fields ```dart { 'posts': { 'orderBy': [ {'priority': 'desc'}, // Primary sort {'createdAt': 'asc'}, // Secondary sort {'title': 'asc'}, // Tertiary sort ] } } ``` ### Dynamic Sorting ```dart // Sort by different fields based on conditions String sortField = userPreference == 'date' ? 'createdAt' : 'title'; String sortDirection = ascending ? 'asc' : 'desc'; final query = { 'posts': { 'orderBy': {sortField: sortDirection}, } }; ``` ## Pagination ### Limit and Offset ```dart { 'posts': { 'orderBy': {'createdAt': 'desc'}, 'limit': 20, // Maximum 20 results 'offset': 40, // Skip first 40 results (page 3) } } ``` InstantDB supports simple offset-based pagination. ```dart // First page { 'posts': { 'orderBy': {'createdAt': 'desc'}, 'limit': 20, 'offset': 0, } } // Next page { 'posts': { 'orderBy': {'createdAt': 'desc'}, 'limit': 20, 'offset': 20, } } ``` ### Pagination Helper ```dart class PaginationHelper { static Map buildQuery({ required String entityType, required int page, required int pageSize, Map? where, Map? orderBy, }) { return { entityType: { if (where != null) 'where': where, if (orderBy != null) 'orderBy': orderBy, 'limit': pageSize, 'offset': page * pageSize, } }; } } // Usage final query = PaginationHelper.buildQuery( entityType: 'posts', page: 2, // Third page (0-indexed) pageSize: 10, where: {'published': true}, orderBy: {'createdAt': 'desc'}, ); ``` ## Relationships and Includes ### Basic Includes ```dart { 'posts': { 'include': { 'author': {}, // Include author for each post 'comments': { // Include comments for each post 'limit': 5, 'orderBy': {'createdAt': 'desc'}, }, } } } ``` ### Nested Includes ```dart { 'posts': { 'include': { 'author': { 'include': { 'profile': {}, // Include author's profile } }, 'comments': { 'include': { 'author': {}, // Include comment authors }, 'orderBy': {'createdAt': 'asc'}, }, } } } ``` ### Conditional Includes ```dart { 'posts': { 'include': { 'author': { 'where': {'active': true}, // Only include if author is active }, 'comments': { 'where': {'approved': true}, // Only approved comments 'limit': 10, }, } } } ``` ### Complex Relationship Queries ```dart // Posts with their authors and latest comments { 'posts': { 'where': {'published': true}, 'include': { 'author': { 'include': { 'profile': {}, } }, 'comments': { 'where': {'approved': true}, 'orderBy': {'createdAt': 'desc'}, 'limit': 3, 'include': { 'author': { 'include': { 'profile': {}, } } } }, 'tags': {}, 'category': {}, }, 'orderBy': {'publishedAt': 'desc'}, 'limit': 10, } } ``` ## Lookups Use lookups to reference entities by attributes other than ID. ### `lookup()` Function ```dart LookupRef lookup(String entityType, String attribute, dynamic value) ``` **Parameters:** - `entityType` (`String`): Type of entity to lookup - `attribute` (`String`): Attribute to match against - `value` (`dynamic`): Value to match **Returns:** `LookupRef` - Lookup reference object ### Lookup Examples ```dart // Reference user by email instead of ID { 'posts': { 'where': { 'author': lookup('users', 'email', 'john@example.com'), } } } // Create post with author lookup await db.transact([ ...db.create('posts', { 'id': db.id(), 'title': 'My Post', 'authorId': lookup('users', 'email', 'author@example.com'), }), ]); // Multiple lookups { 'posts': { 'where': { 'author': lookup('users', 'username', 'john_doe'), 'category': lookup('categories', 'slug', 'technology'), } } } ``` ## Advanced Query Patterns ### Search Functionality ```dart class SearchQueries { static Map searchPosts(String searchTerm) { return { 'posts': { 'where': { '\$or': [ {'title': {'\$ilike': '%$searchTerm%'}}, {'content': {'\$ilike': '%$searchTerm%'}}, {'tags': {'\$contains': searchTerm.toLowerCase()}}, ] }, 'orderBy': {'relevanceScore': 'desc'}, 'limit': 20, } }; } static Map searchUsers(String query) { return { 'users': { 'where': { '\$or': [ {'name': {'\$ilike': '%$query%'}}, {'username': {'\$ilike': '%$query%'}}, {'email': {'\$like': '%$query%'}}, ], 'active': true, // Only active users }, 'orderBy': [ {'verified': 'desc'}, // Verified users first {'name': 'asc'}, // Then by name ], 'limit': 10, } }; } } ``` ### Aggregation Queries ```dart // Get user statistics { 'users': { 'where': {'active': true}, 'include': { 'posts': { 'where': {'published': true}, // Count will be included in results }, 'comments': { 'where': {'approved': true}, }, } } } // Posts with comment counts { 'posts': { 'where': {'published': true}, 'include': { 'comments': { 'where': {'approved': true}, }, 'likes': {}, // Will include like count }, 'orderBy': {'commentCount': 'desc'}, } } ``` ### Time-Based Queries ```dart class TimeQueries { static Map getRecentPosts(Duration duration) { final since = DateTime.now().subtract(duration).millisecondsSinceEpoch; return { 'posts': { 'where': { 'createdAt': {'\$gte': since}, 'published': true, }, 'orderBy': {'createdAt': 'desc'}, } }; } static Map getPostsInRange(DateTime start, DateTime end) { return { 'posts': { 'where': { 'createdAt': { '\$gte': start.millisecondsSinceEpoch, '\$lt': end.millisecondsSinceEpoch, } }, 'orderBy': {'createdAt': 'asc'}, } }; } static Map getTrendingPosts() { final lastWeek = DateTime.now().subtract(Duration(days: 7)).millisecondsSinceEpoch; return { 'posts': { 'where': { 'createdAt': {'\$gte': lastWeek}, 'viewCount': {'\$gt': 100}, }, 'orderBy': [ {'viewCount': 'desc'}, {'likeCount': 'desc'}, {'commentCount': 'desc'}, ], 'limit': 20, } }; } } ``` ### Conditional Queries ```dart class ConditionalQueries { static Map buildUserQuery({ String? role, bool? active, DateTime? createdAfter, List? excludeIds, }) { final where = {}; if (role != null) { where['role'] = role; } if (active != null) { where['active'] = active; } if (createdAfter != null) { where['createdAt'] = {'\$gte': createdAfter.millisecondsSinceEpoch}; } if (excludeIds != null && excludeIds.isNotEmpty) { where['id'] = {'\$nin': excludeIds}; } return { 'users': { if (where.isNotEmpty) 'where': where, 'orderBy': {'createdAt': 'desc'}, } }; } } // Usage final query = ConditionalQueries.buildUserQuery( role: 'user', active: true, createdAfter: DateTime.now().subtract(Duration(days: 30)), excludeIds: ['blocked-user-1', 'blocked-user-2'], ); ``` ## Query Optimization ### Efficient Filtering ```dart // ✅ Good: Filter at database level { 'posts': { 'where': { 'published': true, 'authorId': currentUserId, }, 'limit': 10, } } // ❌ Avoid: Filter in application { 'posts': {} // Gets all posts, then filter in Dart code } ``` ### Index-Friendly Queries ```dart // ✅ Good: Use indexed fields for where conditions { 'posts': { 'where': { 'createdAt': {'\$gte': timestamp}, // Usually indexed 'published': true, // Simple equality } } } // ❌ Less efficient: Complex string operations { 'posts': { 'where': { 'content': {'\$regex': 'complex.*pattern'}, // Expensive } } } ``` ### Limit Result Sets ```dart // Always use appropriate limits { 'posts': { 'where': {'published': true}, 'limit': 20, // Prevent loading too much data 'orderBy': {'createdAt': 'desc'}, } } ``` ## Error Handling Handle query errors appropriately: ```dart Future>> safeQuery(Map query) async { try { final result = await db.queryOnce(query); if (result.error != null) { print('Query error: ${result.error}'); return []; } // Extract first entity type from results final entityType = query.keys.first; return (result.data?[entityType] as List? ?? []) .cast>(); } on InstantException catch (e) { print('InstantDB error: ${e.message}'); return []; } catch (e) { print('Unexpected error: $e'); return []; } } ``` ## Query Builder Helper Create a query builder for complex queries: ```dart class QueryBuilder { final Map _query = {}; QueryBuilder entity(String entityType) { _query[entityType] = {}; return this; } QueryBuilder where(Map conditions) { final entityType = _query.keys.last; _query[entityType]['where'] = conditions; return this; } QueryBuilder orderBy(Map ordering) { final entityType = _query.keys.last; _query[entityType]['orderBy'] = ordering; return this; } QueryBuilder limit(int count) { final entityType = _query.keys.last; _query[entityType]['limit'] = count; return this; } QueryBuilder offset(int count) { final entityType = _query.keys.last; _query[entityType]['offset'] = count; return this; } QueryBuilder include(Map includes) { final entityType = _query.keys.last; _query[entityType]['include'] = includes; return this; } Map build() => Map.from(_query); } // Usage final query = QueryBuilder() .entity('posts') .where({ 'published': true, 'createdAt': {'\$gte': DateTime.now().subtract(Duration(days: 7)).millisecondsSinceEpoch}, }) .orderBy({'createdAt': 'desc'}) .limit(10) .include({ 'author': {}, 'comments': {'limit': 5}, }) .build(); final result = await db.queryOnce(query); ``` ## Next Steps Explore related APIs: - [InstantDB Core](/docs/api/instantdb) - Main database class and initialization - [Transactions API](/docs/api/transactions) - Creating and updating data - [Presence API](/docs/api/presence-api) - Real-time collaboration queries - [Flutter Widgets](/docs/api/widgets) - Reactive query widgets - [Types Reference](/docs/api/types) - Query result types and structures --- # Transactions API > Complete API reference for InstantDB transactions and operations Source: https://flutter-instantdb.vercel.app/docs/api/transactions InstantDB transactions provide atomic operations for creating, updating, and deleting data. All mutations in InstantDB happen within transactions to ensure data consistency and enable optimistic updates. ## Core Concepts ### Transaction Atomicity All operations within a transaction are applied atomically - either all succeed or all fail: ```dart await db.transact([ ...db.create('user', userData), ...db.create('profile', profileData), db.update(settingsId, newSettings), ]); // All operations succeed together or all fail ``` ### Optimistic Updates InstantDB applies transactions optimistically - changes appear immediately in the UI, with automatic rollback if the server rejects the transaction: ```dart // UI updates immediately, sync happens in background await db.transact([ db.update(postId, {'likes': {'$increment': 1}}), ]); ``` ## Transaction Methods ### `transact()` Execute a transaction with operations or transaction chunks. ```dart Future transact(dynamic transaction) ``` **Parameters:** - `transaction`: Either `List` or `TransactionChunk` **Returns:** `Future` ```dart await db.transact([ ...db.create('posts', { 'id': db.id(), 'title': 'Hello World', 'content': 'My first post', }), db.update(userId, {'lastPostAt': DateTime.now().millisecondsSinceEpoch}), ]); ``` ```dart await db.transact( db.tx['posts'][postId].update({ 'title': 'Updated Title', 'updatedAt': DateTime.now().millisecondsSinceEpoch, }) ); ``` ### `TransactionResult` Result object returned by transaction operations. ```dart class TransactionResult { final bool success; final String? error; final Map? data; } ``` **Properties:** - `success` (`bool`): Whether the transaction succeeded - `error` (`String?`): Error message if transaction failed - `data` (`Map?`): Additional result data ## Operation Types ### Create Operations #### `create()` Create a new entity. ```dart List create(String entityType, Map data) ``` **Parameters:** - `entityType` (`String`): Type of entity to create - `data` (`Map`): Entity data (must include `id`) **Returns:** `List` - List containing the create operation **Examples:** ```dart // Basic create await db.transact([ ...db.create('todos', { 'id': db.id(), 'text': 'Learn InstantDB', 'completed': false, 'createdAt': DateTime.now().millisecondsSinceEpoch, }), ]); // Create with relationships await db.transact([ ...db.create('posts', { 'id': db.id(), 'title': 'My Post', 'authorId': userId, 'tags': ['flutter', 'database'], }), ]); // Create multiple entities final postId = db.id(); final commentId = db.id(); await db.transact([ ...db.create('posts', { 'id': postId, 'title': 'Hello World', 'content': 'First post!', }), ...db.create('comments', { 'id': commentId, 'postId': postId, 'text': 'Great post!', 'authorId': userId, }), ]); ``` ### Update Operations #### `update()` Update an existing entity. ```dart Operation update(String entityId, Map data) ``` **Parameters:** - `entityId` (`String`): ID of entity to update - `data` (`Map`): Data to update **Returns:** `Operation` - The update operation **Examples:** ```dart // Basic update await db.transact([ db.update(todoId, { 'completed': true, 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); // Partial update await db.transact([ db.update(postId, { 'title': 'Updated Title', // Only updates title }), ]); // Update with new value (replaces existing field) await db.transact([ db.update(postId, { 'viewCount': 101, 'likes': 5, }), ]); // Update arrays by providing the full new array await db.transact([ db.update(postId, { 'tags': ['flutter', 'database', 'new-tag'], }), ]); ``` #### `merge()` Deep merge data into an entity. ```dart Operation merge(String entityId, Map data) ``` **Parameters:** - `entityId` (`String`): ID of entity to merge into - `data` (`Map`): Data to deep merge **Returns:** `Operation` - The merge operation **Examples:** ```dart // Deep merge nested objects await db.transact([ db.merge(userId, { 'preferences': { 'theme': 'dark', // Updates theme 'notifications': { // Merges with existing notifications 'email': false, // Updates email setting 'push': true, // Updates push setting }, }, 'profile': { 'bio': 'Updated bio', // Updates bio in profile }, }), ]); // Original data: // { // 'preferences': { // 'theme': 'light', // 'notifications': {'email': true, 'sms': true}, // 'language': 'en' // }, // 'profile': {'bio': 'Old bio', 'avatar': 'avatar.png'} // } // // After merge: // { // 'preferences': { // 'theme': 'dark', // ← Updated // 'notifications': {'email': false, 'sms': true, 'push': true}, // ← Merged // 'language': 'en' // ← Preserved // }, // 'profile': {'bio': 'Updated bio', 'avatar': 'avatar.png'} // ← Merged // } ``` ### Delete Operations #### `delete()` Delete an entity. ```dart Operation delete(String entityId) ``` **Parameters:** - `entityId` (`String`): ID of entity to delete **Returns:** `Operation` - The delete operation **Examples:** ```dart // Delete single entity await db.transact([ db.delete(todoId), ]); // Delete multiple entities await db.transact([ db.delete(tagId), ]); // Conditional delete with cleanup final post = await db.queryOnce({'posts': {'where': {'id': postId}}}); if (post.data?['posts']?.isNotEmpty == true) { final postData = post.data!['posts'][0] as Map; final authorId = postData['authorId']; await db.transact([ db.delete(postId), // Update author's last active time instead of increment db.update(authorId, { 'lastActive': DateTime.now().millisecondsSinceEpoch, }), ]); } ``` ### Relationship Operations #### `link()` Create a relationship between entities. ```dart Operation link(String fromId, String linkName, String toId) ``` **Parameters:** - `fromId` (`String`): Source entity ID - `linkName` (`String`): Name of the relationship - `toId` (`String`): Target entity ID **Returns:** `Operation` - The link operation #### `unlink()` Remove a relationship between entities. ```dart Operation unlink(String fromId, String linkName, String toId) ``` **Parameters:** - `fromId` (`String`): Source entity ID - `linkName` (`String`): Name of the relationship - `toId` (`String`): Target entity ID **Returns:** `Operation` - The unlink operation **Examples:** ```dart // Link user to post await db.transact([ db.link(userId, 'posts', postId), ]); // Link post to multiple tags await db.transact([ db.link(postId, 'tags', tag1Id), db.link(postId, 'tags', tag2Id), db.link(postId, 'tags', tag3Id), ]); // Unlink relationship await db.transact([ db.unlink(userId, 'posts', postId), ]); // Replace links (unlink old, link new) await db.transact([ db.unlink(postId, 'category', oldCategoryId), db.link(postId, 'category', newCategoryId), ]); ``` ## Upsert, Strict Mode & Rule Params ### `lookup()` — upsert by unique attribute Target an entity by a unique attribute instead of by id. If a matching entity exists it is updated; otherwise one is created (upsert). `lookup` is chainable like an entity target and supports `update`, `merge`, `delete`, `link`, and `unlink`. ```dart // Update-or-create the profile whose email == 'a@b.com' await db.transact( db.tx['profiles'].lookup('email', 'a@b.com').update({ 'name': 'Ada', 'lastSeen': DateTime.now().millisecondsSinceEpoch, }), ); // Also works with merge / delete / link / unlink await db.transact( db.tx['profiles'].lookup('email', 'a@b.com').merge({ 'preferences': {'theme': 'dark'}, }), ); ``` ### Strict mode with `TxOpts(upsert: false)` By default `update` / `merge` upsert — they create the entity if it does not exist. Pass `opts: TxOpts(upsert: false)` to make the write a no-op when the target does not exist. ```dart await db.transact( db.tx['goals'][goalId].update( {'title': 'Updated'}, opts: const TxOpts(upsert: false), // does not create if missing ), ); ``` ### `ruleParams()` Attach rule parameters forwarded to the server for permission rules. Chain it after a write op. ```dart await db.transact( db.tx['docs'][docId] .update({'title': 'Shared doc'}) .ruleParams({'inviteToken': token}), ); ``` ## New Transaction API (tx namespace) ### `TransactionNamespace` The new fluent transaction API provides a more intuitive way to build complex operations. ```dart TransactionNamespace get tx ``` **Access pattern:** ```dart db.tx[entityType][entityId].method(data) ``` ### Fluent Operations #### `update()` ```dart TransactionChunk update(Map data) ``` **Example:** ```dart await db.transact( db.tx['users'][userId].update({ 'name': 'New Name', 'updatedAt': DateTime.now().millisecondsSinceEpoch, }) ); ``` #### `merge()` ```dart TransactionChunk merge(Map data) ``` **Example:** ```dart await db.transact( db.tx['users'][userId].merge({ 'preferences': { 'theme': 'dark', 'notifications': {'email': false}, }, }) ); ``` #### `link()` ```dart TransactionChunk link(Map> links) ``` **Example:** ```dart await db.transact( db.tx['users'][userId].link({ 'posts': [postId1, postId2], 'groups': [groupId], }) ); ``` #### `unlink()` ```dart TransactionChunk unlink(Map> links) ``` **Example:** ```dart await db.transact( db.tx['users'][userId].unlink({ 'posts': [oldPostId], }) ); ``` ### Chaining Operations Chain multiple operations on the same entity: ```dart await db.transact( db.tx['users'][userId] .update({'name': 'New Name'}) .merge({'preferences': {'theme': 'dark'}}) .link({'groups': [groupId]}) ); ``` ### Complex Transaction Examples ```dart // Blog post creation with full relationships final postId = db.id(); final authorId = db.auth.currentUser.value!.id; await db.transact( db.tx['users'][authorId] .update({'lastPostId': postId}) .link({'posts': [postId]}) ); await db.transact([ ...db.create('posts', { 'id': postId, 'title': 'My New Post', 'content': 'Post content here...', 'authorId': authorId, 'publishedAt': DateTime.now().millisecondsSinceEpoch, }), ]); // User profile update with multiple relationships await db.transact( db.tx['users'][userId] .merge({ 'profile': { 'bio': 'Updated bio', 'website': 'https://example.com', }, 'preferences': { 'emailNotifications': true, }, }) .link({ 'followers': [followerId1, followerId2], 'interests': [interestId1, interestId2], }) .unlink({ 'blockedUsers': [unblockedUserId], }) ); ``` ## Advanced Operations ### Conditional Updates Update entities only if they meet certain conditions: ```dart // Check condition first final result = await db.queryOnce({ 'posts': {'where': {'id': postId}}, }); final posts = result.data?['posts'] as List?; if (posts?.isNotEmpty == true) { final post = posts!.first as Map; // Only update if not already published if (post['status'] != 'published') { await db.transact([ db.update(postId, { 'status': 'published', 'publishedAt': DateTime.now().millisecondsSinceEpoch, }), ]); } } ``` ### Batch Operations Process large numbers of operations efficiently: ```dart class BatchProcessor { final InstantDB db; static const int batchSize = 50; BatchProcessor(this.db); Future processBatch(List operations) 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 ${(i / batchSize).floor() + 1} failed: $e'); rethrow; } // Small delay to avoid overwhelming the system await Future.delayed(const Duration(milliseconds: 100)); } } } // Usage final processor = BatchProcessor(db); final operations = []; // Add many operations for (int i = 0; i < 500; i++) { operations.addAll(db.create('items', { 'id': db.id(), 'name': 'Item $i', 'value': i, })); } await processor.processBatch(operations); ``` ### Transaction Validation Validate data before transactions: ```dart class TransactionValidator { static void validateTodo(Map data) { if (!data.containsKey('text') || data['text']?.toString().trim().isEmpty) { throw InstantException( message: 'Todo text is required', code: 'validation_error', ); } if (data['text'].toString().length > 1000) { throw InstantException( message: 'Todo text too long (max 1000 characters)', code: 'validation_error', ); } } static void validateUser(Map data) { if (data.containsKey('email')) { final email = data['email']?.toString() ?? ''; if (!RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email)) { throw InstantException( message: 'Invalid email format', code: 'validation_error', ); } } } } // Usage Future createTodoSafely(String text) async { final todoData = { 'id': db.id(), 'text': text, 'completed': false, 'createdAt': DateTime.now().millisecondsSinceEpoch, }; try { TransactionValidator.validateTodo(todoData); await db.transact([ ...db.create('todos', todoData), ]); } on InstantException catch (e) { if (e.code == 'validation_error') { // Handle validation error showError('Validation Error: ${e.message}'); } else { rethrow; } } } ``` ```dart await db.transact([ db.update(entityId, { 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); ``` ## Error Handling Handle transaction errors appropriately: ```dart Future safeTransaction(List operations) async { try { final result = await db.transact(operations); if (!result.success) { print('Transaction failed: ${result.error}'); return; } print('Transaction completed successfully'); } on InstantException catch (e) { switch (e.code) { case 'validation_error': print('Validation failed: ${e.message}'); // Show user-friendly validation error break; case 'network_error': print('Network error: ${e.message}'); // Retry or show offline message break; case 'auth_error': print('Authentication error: ${e.message}'); // Redirect to login break; case 'permission_denied': print('Permission denied: ${e.message}'); // Show access denied message break; default: print('Unknown error: ${e.message}'); // Generic error handling } } catch (e) { print('Unexpected error: $e'); // Handle unexpected errors } } ``` ## Best Practices ### 1. Use Appropriate IDs Always use `db.id()` for entity IDs: ```dart // ✅ Good: Use generated UUIDs await db.transact([ ...db.create('posts', { 'id': db.id(), // Proper UUID 'title': 'My Post', }), ]); // ❌ Avoid: Custom string IDs await db.transact([ ...db.create('posts', { 'id': 'my-custom-id', // May cause server errors 'title': 'My Post', }), ]); ``` ### 2. Group Related Operations Batch related operations in single transactions: ```dart // ✅ Good: Atomic operation await db.transact([ ...db.create('order', orderData), db.update(productId, {'stock': {'$decrement': 1}}), db.update(userId, {'orderCount': {'$increment': 1}}), ]); // ❌ Avoid: Separate transactions await db.transact([...db.create('order', orderData)]); await db.transact([db.update(productId, {'stock': {'$decrement': 1}})]); await db.transact([db.update(userId, {'orderCount': {'$increment': 1}})]); ``` ### 3. Validate Before Transacting Always validate data before sending to the server: ```dart // Validate data structure and constraints void validateBeforeCreate(Map data) { if (!data.containsKey('id')) { throw ArgumentError('Entity must have an ID'); } if (data['id'] == null || data['id'].toString().isEmpty) { throw ArgumentError('ID cannot be empty'); } } ``` ### 4. Handle Optimistic Update Failures Be prepared for optimistic updates to fail: ```dart Future optimisticUpdate(String entityId, Map data) async { try { await db.transact([db.update(entityId, data)]); } catch (e) { // Update failed - UI will automatically revert print('Optimistic update failed: $e'); // Optionally show user feedback showSnackBar('Update failed, please try again'); } } ``` ## Next Steps Explore related APIs: - [InstantDB Core](/docs/api/instantdb) - Main database class and methods - [Queries API](/docs/api/queries) - Advanced querying capabilities - [Presence API](/docs/api/presence-api) - Real-time collaboration - [Flutter Widgets](/docs/api/widgets) - Reactive UI components - [Types Reference](/docs/api/types) - Complete type definitions --- # Types Reference > Complete type definitions and interfaces for Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/api/types This reference documents the core types and classes used in Flutter InstantDB. ## Core Classes ### `InstantDB` Main database class providing all database operations. ```dart class InstantDB { String get appId; InstantConfig get config; AuthManager get auth; PresenceManager get presence; TransactionBuilder get tx; // Reactive signals Signal get isReady; Signal get isOnline; // Initialization static Future init({ required String appId, InstantConfig? config, InstantSchema? schema, }); // Query methods Signal query(Map query, {bool syncedOnly = false}); Future queryOnce(Map query, {bool syncedOnly = false}); // Transaction methods Future transact(dynamic transaction); // Auth methods Stream subscribeAuth(); // Utilities String id(); } ``` ### `InstantConfig` Configuration options for database initialization. ```dart class InstantConfig { const InstantConfig({ this.persistenceDir, this.syncEnabled = true, this.baseUrl = 'https://api.instantdb.com', this.maxCacheSize = 50 * 1024 * 1024, this.reconnectDelay = const Duration(seconds: 1), this.verboseLogging = false, }); final String? persistenceDir; final bool syncEnabled; final String baseUrl; final int maxCacheSize; final Duration reconnectDelay; final bool verboseLogging; } ``` ### `QueryResult` Result returned by query operations. ```dart class QueryResult { final Map? data; final String? error; final bool isLoading; bool get hasData => data != null; bool get hasError => error != null; } ``` ### `AuthUser` Represents an authenticated user. ```dart class AuthUser { final String id; final String email; final DateTime createdAt; final Map? metadata; } ``` ### `TransactionResult` Result returned by transaction operations. ```dart class TransactionResult { final bool success; final String? error; final Map? data; } ``` ## Presence Types ### `PresenceData` Represents a user's presence state in a room. ```dart class PresenceData { final String userId; final Map data; final DateTime lastSeen; } ``` ### `CursorData` Represents cursor position and metadata. ```dart class CursorData { final String userId; final String? userName; final String? userColor; final double x; final double y; final Map? metadata; final DateTime lastUpdated; } ``` ### `ReactionData` Represents a reaction sent to a room. ```dart class ReactionData { final String id; final String userId; final String roomId; final String emoji; final Map? metadata; final DateTime timestamp; } ``` ## Transaction Internals ### `Operation` Represents a single database mutation. ```dart class Operation { final OperationType type; final String entityType; final String entityId; final Map? data; } enum OperationType { add, update, delete, merge, link, unlink, retract } ``` ### `LookupRef` Reference used to look up entities by an attribute other than `id`. ```dart class LookupRef { final String entityType; final String attribute; final dynamic value; } ``` ### `TransactionChunk` A wrapper for a list of operations, often generated by the fluent `tx` API. ```dart class TransactionChunk { final List operations; } ``` --- # Flutter Widgets > Complete reference for Flutter InstantDB reactive widgets Source: https://flutter-instantdb.vercel.app/docs/api/widgets Flutter InstantDB provides a set of reactive widgets that automatically update when data changes. These widgets integrate seamlessly with Flutter's widget tree and provide optimized performance. ## Core Widgets ### `InstantProvider` Provides the `InstantDB` instance to the widget tree using Flutter's `InheritedWidget` pattern. This should typically wrap your entire app. ```dart class InstantProvider extends InheritedWidget { const InstantProvider({ super.key, required this.db, required super.child, }); final InstantDB db; static InstantDB of(BuildContext context) { // Returns the InstantDB instance } } ``` **Parameters:** - `db` (`InstantDB`): The database instance to provide - `child` (`Widget`): The child widget tree **Example:** ```dart void main() async { final db = await InstantDB.init(appId: 'your-app-id'); runApp(InstantProvider(db: db, child: MyApp())); } ``` ### `Watch` Part of the `signals_flutter` library (re-exported by `flutter_instantdb`). This widget rebuilds whenever any signal accessed inside its builder function changes value. ```dart class Watch extends StatelessWidget { const Watch(this.builder, {super.key}); final Widget Function(BuildContext context) builder; } ``` **Example:** ```dart Watch((context) { final db = InstantProvider.of(context); final isOnline = db.isOnline.value; // Accessing .value creates a dependency return Text(isOnline ? 'Connected' : 'Disconnected'); }); ``` ## Query Widgets ### `InstantBuilder` The primary widget for building UI based on InstaQL queries. It handles loading and error states automatically. ```dart class InstantBuilder extends StatelessWidget { const InstantBuilder({ required this.query, required this.builder, this.errorBuilder, this.loadingBuilder, }); } ``` **Properties:** - `query` (`Map`): The InstaQL query - `builder` (`Widget Function(BuildContext, Map)`): Called when data is available - `loadingBuilder` (`Widget Function(BuildContext)?`): Optional custom loading UI - `errorBuilder` (`Widget Function(BuildContext, String)?`): Optional custom error UI **Example:** ```dart InstantBuilder( query: {'goals': {}}, builder: (context, data) { final goals = data['goals'] as List; return ListView(children: [ ... ]); }, ) ``` ### `InstantBuilderTyped` A generic version of `InstantBuilder` that includes a `transformer` function to convert raw map data into your own model classes. **Example:** ```dart InstantBuilderTyped>( query: {'todos': {}}, transformer: (data) => (data['todos'] as List) .map((json) => Todo.fromJson(json)) .toList(), builder: (context, todos) { return TodoList(todos: todos); }, ) ``` ### `InstantBuilder.list` / `InstantBuilder.single` Convenience static methods for common query patterns. ```dart // Fetch a list of entities InstantBuilder.list( entityType: 'todos', where: {'completed': false}, builder: (context, todos) => ..., ) // Fetch a single entity by ID InstantBuilder.single( entityType: 'users', id: 'user-123', builder: (context, user) => ..., ) ``` ## Authentication Widgets ### `AuthBuilder` Rebuilds when the authentication state changes. ```dart AuthBuilder( builder: (context, user) { if (user == null) return Text('Not logged in'); return Text('Logged in as ${user.email}'); }, ) ``` ### `AuthGuard` A higher-level widget that shows a `child` if the user is authenticated, or a `fallback` (or `loginBuilder`) if they are not. ```dart AuthGuard( fallback: LoadingScreen(), loginBuilder: (context) => LoginScreen(), child: DashboardScreen(), ) ``` ### `OAuthButton` A drop-in OAuth sign-in button. Builds an authorization URL (PKCE on by default) and hands the `OAuthFlow` to `onLaunch`; your app opens it and calls `db.auth.exchangeCodeForToken`. Does not bundle `url_launcher`. ```dart OAuthButton( provider: OAuthProvider.google, // default label/color/icon clientName: 'google', // OAuth client from the dashboard redirectUri: 'myapp://oauth', onLaunch: (flow) async { final result = await FlutterWebAuth2.authenticate( url: flow.url, callbackUrlScheme: 'myapp'); final code = Uri.parse(result).queryParameters['code']!; await db.auth.exchangeCodeForToken( code: code, codeVerifier: flow.codeVerifier); }, ) ``` `OAuthProvider` values: `google`, `apple`, `github`, `linkedin`, `clerk`, `firebase`. Override the preset with `label`, `color`, or `icon`; pass `scopes` or `usePKCE: false` as needed. See [User Management](/docs/auth/users#oauth-sign-in). ## Presence Widgets Reactive equivalents of InstantDB's React presence hooks. Each joins the room and rebuilds on change. See [Presence System](/docs/realtime/presence#reactive-presence-widgets). ### `PresenceBuilder` Equivalent of `usePresence`. Rebuilds on peer presence changes. ```dart PresenceBuilder( roomId: 'doc-42', initialPresence: {'name': 'Alice'}, builder: (context, room, peers) => Text('${peers.length} online'), ) ``` ### `TopicListener` Equivalent of `useTopicEffect`. Side-effect widget; renders `child`. ```dart TopicListener( roomId: 'doc-42', topic: 'emoji', onEvent: (data) => _showFloatingEmoji(data['emoji']), child: const Editor(), ) ``` ### `TypingIndicatorBuilder` Rebuilds with the current typing peers. ```dart TypingIndicatorBuilder( roomId: 'doc-42', builder: (context, typing) => typing.isEmpty ? const SizedBox() : Text('${typing.length} typing…'), ) ``` ### `ReactionsBuilder` Rebuilds with the live list of reactions. ```dart ReactionsBuilder( roomId: 'doc-42', builder: (context, reactions) => Wrap(children: [for (final r in reactions) Text(r.emoji)]), ) ``` ### `CursorOverlay` Multiplayer cursor layer — Flutter equivalent of ``. ```dart CursorOverlay( roomId: 'doc-42', userName: 'Alice', userColor: '#E91E63', child: const Canvas(), ) ``` ## Connection Widgets ### `ConnectionStatusBuilder` Provides periodic updates on the database connection status. ```dart ConnectionStatusBuilder( builder: (context, isOnline) { return Icon( isOnline ? Icons.cloud_done : Icons.cloud_off, color: isOnline ? Colors.green : Colors.red, ); }, ) ``` ## State Management Hooks ### `useInstantQuery` A function that returns a `Signal` for a given query. Useful inside `StatefulWidget` or other places where you want to manage the signal lifecycle yourself. ```dart class MyWidget extends StatefulWidget { @override _MyWidgetState createState() => _MyWidgetState(); } class _MyWidgetState extends State { late Signal query; @override void didChangeDependencies() { super.didChangeDependencies(); query = useInstantQuery(context, {'messages': {}}); } @override Widget build(BuildContext context) { return Watch((context) { final result = query.value; // ... build UI }); } } ``` --- # Permissions & Access Control > Role-based access control and permission management in Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/auth/permissions InstantDB provides flexible permission systems through user roles, entity-level access control, and custom permission logic. Build secure applications with fine-grained access control that scales with your needs. ## Understanding Permissions ### Permission Concepts InstantDB permissions are based on several key concepts: - **User Roles**: Define what a user can do (admin, editor, viewer) - **Entity Access**: Control access to specific data entities - **Operation Permissions**: Control create, read, update, delete operations - **Attribute-level Access**: Control access to specific fields ## User Roles ### Basic Role System Implement a simple role-based system: ```dart enum UserRole { admin, editor, viewer, guest, } extension UserRoleExtension on UserRole { String get displayName { switch (this) { case UserRole.admin: return 'Administrator'; case UserRole.editor: return 'Editor'; case UserRole.viewer: return 'Viewer'; case UserRole.guest: return 'Guest'; } } bool get canWrite { return this == UserRole.admin || this == UserRole.editor; } bool get canDelete { return this == UserRole.admin; } bool get canManageUsers { return this == UserRole.admin; } } class UserPermissions { final AuthUser user; late final UserRole role; UserPermissions(this.user) { // Extract role from user metadata final roleString = user.type as String?; role = UserRole.values.firstWhere( (r) => r.name == roleString, orElse: () => UserRole.guest, ); } bool canRead(String entityType) { // All authenticated users can read return true; } bool canCreate(String entityType) { switch (entityType) { case 'posts': case 'comments': return role.canWrite; case 'users': return role.canManageUsers; default: return role.canWrite; } } bool canUpdate(String entityType, Map entity) { // Users can update their own content if (entity['authorId'] == user.id) return true; // Admins can update anything if (role == UserRole.admin) return true; // Editors can update posts and comments if (role == UserRole.editor && ['posts', 'comments'].contains(entityType)) { return true; } return false; } bool canDelete(String entityType, Map entity) { // Only admins can delete, or users can delete their own content return role == UserRole.admin || entity['authorId'] == user.id; } } ``` ### Role Management UI Create interfaces for managing user roles: ```dart class RoleManagementScreen extends StatefulWidget { @override State createState() => _RoleManagementScreenState(); } class _RoleManagementScreenState extends State { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value!; final userPermissions = UserPermissions(currentUser); if (!userPermissions.canManageUsers) { return Scaffold( appBar: AppBar(title: const Text('Access Denied')), body: const Center( child: Text('You do not have permission to manage users.'), ), ); } return Scaffold( appBar: AppBar( title: const Text('Role Management'), ), body: InstantBuilder( query: { 'users': { 'orderBy': {'createdAt': 'desc'}, } }, builder: (context, result) { final users = (result.data?['users'] as List? ?? []) .cast>(); return ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return UserRoleCard( user: user, onRoleChanged: (newRole) => _updateUserRole(user['id'], newRole), ); }, ); }, ), ); } Future _updateUserRole(String userId, UserRole newRole) async { final db = InstantProvider.of(context); await db.transact([ db.update(userId, { 'role': newRole.name, 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('User role updated to ${newRole.displayName}'), ), ); } } class UserRoleCard extends StatelessWidget { final Map user; final Function(UserRole) onRoleChanged; const UserRoleCard({ super.key, required this.user, required this.onRoleChanged, }); @override Widget build(BuildContext context) { final currentRole = UserRole.values.firstWhere( (r) => r.name == (user['role'] as String? ?? 'guest'), orElse: () => UserRole.guest, ); return Card( margin: const EdgeInsets.all(8), child: ListTile( leading: CircleAvatar( child: Text(user['email']?.toString().substring(0, 1).toUpperCase() ?? '?'), ), title: Text(user['email']?.toString() ?? 'Unknown'), subtitle: Text('Role: ${currentRole.displayName}'), trailing: DropdownButton( value: currentRole, onChanged: (newRole) { if (newRole != null) { onRoleChanged(newRole); } }, items: UserRole.values.map((role) { return DropdownMenuItem( value: role, child: Text(role.displayName), ); }).toList(), ), ), ); } } ``` ## Entity-Level Permissions ### Ownership-Based Access Implement ownership-based permissions: ```dart class OwnershipPermissions { final AuthUser currentUser; OwnershipPermissions(this.currentUser); bool canAccess(Map entity, String operation) { switch (operation) { case 'read': return _canRead(entity); case 'update': return _canUpdate(entity); case 'delete': return _canDelete(entity); default: return false; } } bool _canRead(Map entity) { // Public entities can be read by anyone if (entity['isPublic'] == true) return true; // Private entities can only be read by owner if (entity['ownerId'] == currentUser.id) return true; // Shared entities can be read by collaborators final collaborators = entity['collaborators'] as List?; if (collaborators?.contains(currentUser.id) == true) return true; return false; } bool _canUpdate(Map entity) { // Only owner or collaborators with edit permission if (entity['ownerId'] == currentUser.id) return true; final permissions = entity['permissions'] as Map?; final userPermission = permissions?[currentUser.id] as String?; return userPermission == 'edit' || userPermission == 'admin'; } bool _canDelete(Map entity) { // Only owner can delete return entity['ownerId'] == currentUser.id; } } // Usage in widgets class SecurePostList extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value!; final permissions = OwnershipPermissions(currentUser); return InstantBuilder( query: {'posts': {}}, builder: (context, result) { final posts = (result.data?['posts'] as List? ?? []) .cast>() .where((post) => permissions.canAccess(post, 'read')) .toList(); return ListView.builder( itemCount: posts.length, itemBuilder: (context, index) { final post = posts[index]; return PostCard( post: post, canEdit: permissions.canAccess(post, 'update'), canDelete: permissions.canAccess(post, 'delete'), ); }, ); }, ); } } ``` ### Team-Based Permissions Implement team-based access control: ```dart class TeamPermissions { final AuthUser currentUser; final Map team; TeamPermissions({required this.currentUser, required this.team}); UserRole get userRoleInTeam { final members = team['members'] as Map? ?? {}; final roleString = members[currentUser.id] as String?; return UserRole.values.firstWhere( (r) => r.name == roleString, orElse: () => UserRole.guest, ); } bool get isMember => userRoleInTeam != UserRole.guest; bool get isAdmin => userRoleInTeam == UserRole.admin; bool get canEdit => userRoleInTeam.canWrite; bool canAccessProject(Map project) { // Check if project belongs to team if (project['teamId'] != team['id']) return false; // Check if user has access to team return isMember; } bool canManageTeam() { return isAdmin; } bool canInviteMembers() { return userRoleInTeam == UserRole.admin || userRoleInTeam == UserRole.editor; } } // Team access control widget class TeamAccessControl extends StatelessWidget { final Map team; final Widget child; const TeamAccessControl({ super.key, required this.team, required this.child, }); @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; if (currentUser == null) { return const Center(child: Text('Please sign in')); } final teamPermissions = TeamPermissions( currentUser: currentUser, team: team, ); if (!teamPermissions.isMember) { return Scaffold( appBar: AppBar(title: const Text('Access Denied')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.lock, size: 64, color: Colors.grey), const SizedBox(height: 16), Text( 'You don\'t have access to this team', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 24), ElevatedButton( onPressed: () => _requestAccess(context), child: const Text('Request Access'), ), ], ), ), ); } return child; } void _requestAccess(BuildContext context) { // Implementation for requesting team access ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Access request sent to team admins')), ); } } ``` ## Field-Level Permissions ### Attribute Access Control Control access to specific fields: ```dart class AttributePermissions { final AuthUser currentUser; final UserRole role; AttributePermissions(this.currentUser, this.role); Map filterReadableFields( String entityType, Map entity, ) { final filtered = {}; entity.forEach((key, value) { if (canReadField(entityType, key, entity)) { filtered[key] = value; } }); return filtered; } bool canReadField( String entityType, String fieldName, Map entity, ) { switch (entityType) { case 'users': return _canReadUserField(fieldName, entity); case 'posts': return _canReadPostField(fieldName, entity); default: return true; // Default allow read } } bool canWriteField( String entityType, String fieldName, Map entity, ) { switch (entityType) { case 'users': return _canWriteUserField(fieldName, entity); case 'posts': return _canWritePostField(fieldName, entity); default: return role.canWrite; } } bool _canReadUserField(String fieldName, Map entity) { // Public fields anyone can read const publicFields = {'id', 'name', 'avatar', 'createdAt'}; if (publicFields.contains(fieldName)) return true; // Private fields only owner or admin can read const privateFields = {'email', 'phone', 'address'}; if (privateFields.contains(fieldName)) { return entity['id'] == currentUser.id || role == UserRole.admin; } return true; } bool _canWriteUserField(String fieldName, Map entity) { // Users can edit their own profile if (entity['id'] == currentUser.id) { const editableFields = {'name', 'avatar', 'bio'}; return editableFields.contains(fieldName); } // Admins can edit role and status if (role == UserRole.admin) { const adminFields = {'role', 'status', 'permissions'}; return adminFields.contains(fieldName); } return false; } bool _canReadPostField(String fieldName, Map entity) { // Draft posts only visible to author if (entity['status'] == 'draft') { return entity['authorId'] == currentUser.id || role == UserRole.admin; } return true; } bool _canWritePostField(String fieldName, Map entity) { // Author can edit content fields if (entity['authorId'] == currentUser.id) { const contentFields = {'title', 'content', 'tags'}; return contentFields.contains(fieldName); } // Admins can edit metadata if (role == UserRole.admin) { const metaFields = {'status', 'featured', 'priority'}; return metaFields.contains(fieldName); } return false; } } ``` ### Secure Form Builder Build forms that respect field permissions: ```dart class SecureFormBuilder extends StatelessWidget { final String entityType; final Map? initialData; final Function(Map) onSubmit; const SecureFormBuilder({ super.key, required this.entityType, this.initialData, required this.onSubmit, }); @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value!; final userPermissions = UserPermissions(currentUser); final attributePermissions = AttributePermissions( currentUser, userPermissions.role, ); return FormBuilder( entityType: entityType, initialData: initialData, fieldBuilder: (fieldName, fieldData) { final canRead = attributePermissions.canReadField( entityType, fieldName, initialData ?? {}, ); final canWrite = attributePermissions.canWriteField( entityType, fieldName, initialData ?? {}, ); if (!canRead) return const SizedBox.shrink(); return FormField( name: fieldName, data: fieldData, enabled: canWrite, readOnly: !canWrite, ); }, onSubmit: onSubmit, ); } } class FormBuilder extends StatefulWidget { final String entityType; final Map? initialData; final Widget Function(String fieldName, dynamic fieldData) fieldBuilder; final Function(Map) onSubmit; const FormBuilder({ super.key, required this.entityType, this.initialData, required this.fieldBuilder, required this.onSubmit, }); @override State createState() => _FormBuilderState(); } class _FormBuilderState extends State { final _formKey = GlobalKey(); final Map _formData = {}; @override void initState() { super.initState(); _formData.addAll(widget.initialData ?? {}); } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ // Build form fields based on permissions ...(_getFieldSchema()[widget.entityType] as Map) .entries .map((entry) => widget.fieldBuilder(entry.key, entry.value)), const SizedBox(height: 24), ElevatedButton( onPressed: _submitForm, child: Text(widget.initialData != null ? 'Update' : 'Create'), ), ], ), ); } Map _getFieldSchema() { // Define your entity field schemas here return { 'posts': { 'title': {'type': 'string', 'required': true}, 'content': {'type': 'text', 'required': true}, 'tags': {'type': 'array'}, 'status': {'type': 'select', 'options': ['draft', 'published']}, }, 'users': { 'name': {'type': 'string', 'required': true}, 'email': {'type': 'email', 'required': true}, 'role': {'type': 'select', 'options': ['admin', 'editor', 'viewer']}, 'bio': {'type': 'text'}, }, }; } void _submitForm() { if (_formKey.currentState!.validate()) { widget.onSubmit(_formData); } } } ``` ## Permission Middleware ### Query-Level Permissions Apply permissions at the query level: ```dart class PermissionMiddleware { final AuthUser currentUser; final UserPermissions permissions; PermissionMiddleware(this.currentUser, this.permissions); Map applyReadPermissions(Map query) { final modifiedQuery = Map.from(query); modifiedQuery.forEach((entityType, querySpec) { if (!permissions.canRead(entityType)) { // Remove entities user can't read modifiedQuery.remove(entityType); return; } // Add ownership filters for private data final spec = querySpec as Map; final where = spec['where'] as Map? ?? {}; switch (entityType) { case 'private_posts': where['authorId'] = currentUser.id; break; case 'team_documents': where['teamMembers'] = {'\$contains': currentUser.id}; break; } if (where.isNotEmpty) { spec['where'] = where; } }); return modifiedQuery; } List applyWritePermissions(List operations) { return operations.where((op) { switch (op.type) { case 'create': return permissions.canCreate(op.entityType); case 'update': return permissions.canUpdate(op.entityType, op.data); case 'delete': return permissions.canDelete(op.entityType, op.data); default: return false; } }).toList(); } } // Secure database wrapper class SecureInstantDB { final InstantDB _db; final PermissionMiddleware _middleware; SecureInstantDB(this._db) : _middleware = PermissionMiddleware( _db.auth.currentUser.value!, UserPermissions(_db.auth.currentUser.value!), ); Signal subscribeQuery(Map query) { final secureQuery = _middleware.applyReadPermissions(query); return _db.subscribeQuery(secureQuery); } Future transact(List operations) async { final allowedOperations = _middleware.applyWritePermissions(operations); if (allowedOperations.isEmpty) { throw InstantException( message: 'No operations allowed for current user', code: 'permission_denied', ); } return await _db.transact(allowedOperations); } } ``` ## Advanced Permissions ### Dynamic Permission Rules Implement context-aware permissions: ```dart class DynamicPermissions { final AuthUser currentUser; final DateTime currentTime; final String userLocation; DynamicPermissions({ required this.currentUser, required this.currentTime, required this.userLocation, }); bool canAccess(String resource, Map context) { // Time-based permissions if (_hasTimeRestriction(resource)) { if (!_isWithinAllowedTime(resource)) return false; } // Location-based permissions if (_hasLocationRestriction(resource)) { if (!_isInAllowedLocation(resource)) return false; } // Context-based permissions if (_hasContextRestriction(resource)) { if (!_contextAllows(resource, context)) return false; } return true; } bool _hasTimeRestriction(String resource) { const timeRestrictedResources = {'admin_panel', 'financial_reports'}; return timeRestrictedResources.contains(resource); } bool _isWithinAllowedTime(String resource) { // Example: Admin panel only accessible during business hours if (resource == 'admin_panel') { final hour = currentTime.hour; return hour >= 9 && hour <= 17; // 9 AM to 5 PM } return true; } bool _hasLocationRestriction(String resource) { const locationRestrictedResources = {'sensitive_data'}; return locationRestrictedResources.contains(resource); } bool _isInAllowedLocation(String resource) { // Example: Sensitive data only accessible from office locations const allowedLocations = ['office_ny', 'office_sf']; return allowedLocations.contains(userLocation); } bool _hasContextRestriction(String resource) { return true; // Most resources have context restrictions } bool _contextAllows(String resource, Map context) { switch (resource) { case 'edit_post': // Can edit if owner or if granted explicit permission return context['ownerId'] == currentUser.id || context['editors']?.contains(currentUser.id) == true; case 'view_analytics': // Can view analytics if admin or if data relates to user's projects final userRole = currentUser.metadata?['role']; if (userRole == 'admin') return true; final projectIds = context['projectIds'] as List?; final userProjects = currentUser.metadata?['projects'] as List?; return projectIds?.any((id) => userProjects?.contains(id) == true) == true; default: return true; } } } ``` ## Best Practices ### 1. Fail-Safe Defaults Always default to denying access: ```dart class SecureByDefault { static bool checkPermission(String action, {required bool explicit}) { // Require explicit permission grants return explicit; } static List filterByAccess( List items, bool Function(T item) canAccess, ) { return items.where(canAccess).toList(); } } ``` ### 2. Audit Permission Changes Log all permission-related activities: ```dart class PermissionAudit { static void logPermissionChange({ required String userId, required String action, required String resource, required bool granted, }) { final logEntry = { 'timestamp': DateTime.now().millisecondsSinceEpoch, 'userId': userId, 'action': action, 'resource': resource, 'granted': granted, }; print('Permission audit: $logEntry'); // Send to logging service } } ``` ### 3. Test Permission Logic Thoroughly test permission scenarios: ```dart void testPermissions() { final adminUser = AuthUser.test(role: 'admin'); final editorUser = AuthUser.test(role: 'editor'); final viewerUser = AuthUser.test(role: 'viewer'); final adminPerms = UserPermissions(adminUser); final editorPerms = UserPermissions(editorUser); final viewerPerms = UserPermissions(viewerUser); // Test admin permissions assert(adminPerms.canDelete('posts')); assert(adminPerms.canManageUsers()); // Test editor permissions assert(editorPerms.canWrite); assert(!editorPerms.canManageUsers()); // Test viewer permissions assert(!viewerPerms.canWrite); assert(viewerPerms.canRead('posts')); } ``` ### 4. Handle Permission Errors Gracefully Provide clear feedback for permission issues: ```dart class PermissionErrorHandler { static void handlePermissionError( BuildContext context, String action, String resource, ) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Access Denied'), content: Text( 'You don\'t have permission to $action $resource. ' 'Contact your administrator if you need access.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('OK'), ), TextButton( onPressed: () => _requestPermission(context, action, resource), child: const Text('Request Access'), ), ], ), ); } static void _requestPermission( BuildContext context, String action, String resource, ) { // Implement permission request logic Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Permission request sent to administrators'), ), ); } } ``` ## Next Steps Learn more about authentication and security: - [User Management](/docs/auth/users) - User authentication and profiles - [Session Management](/docs/auth/sessions) - Session handling and security - [Advanced Security](/docs/advanced/troubleshooting) - Advanced security patterns - [Data Validation](/docs/getting-started/schema) - Schema-based validation --- # Session Management > Managing user sessions, tokens, and authentication persistence in Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/auth/sessions Flutter InstantDB provides robust session management with automatic token handling, refresh mechanisms, and secure storage. Sessions persist across app restarts and handle token expiration gracefully. ## Session Lifecycle ### Automatic Session Management InstantDB automatically handles session creation, persistence, and cleanup: ```dart final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, // Sessions are automatically managed ), ); // Check if user has an existing session final existingUser = db.auth.currentUser.value; if (existingUser != null) { print('User already authenticated: ${existingUser.email}'); } else { print('No active session'); } ``` ### Session State Monitoring Monitor session changes in real-time: ```dart class SessionMonitor extends StatefulWidget { @override State createState() => _SessionMonitorState(); } class _SessionMonitorState extends State { StreamSubscription? _authSubscription; SessionInfo? _currentSession; @override void initState() { super.initState(); _monitorSession(); } void _monitorSession() { final db = InstantProvider.of(context); _authSubscription = db.auth.onAuthStateChange.listen((user) { setState(() { if (user != null) { _currentSession = SessionInfo( userId: user.id, email: user.email, createdAt: user.createdAt, lastActivity: DateTime.now(), isActive: true, ); } else { _currentSession = null; } }); }); } @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Session Status', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), if (_currentSession != null) ...[ _buildSessionDetail('User ID', _currentSession!.userId), _buildSessionDetail('Email', _currentSession!.email), _buildSessionDetail('Created', _formatDate(_currentSession!.createdAt)), _buildSessionDetail('Last Activity', _formatDate(_currentSession!.lastActivity)), _buildSessionDetail('Status', _currentSession!.isActive ? 'Active' : 'Inactive'), ] else ...[ const Text('No active session'), ], ], ), ), ); } Widget _buildSessionDetail(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 100, child: Text( '$label:', style: const TextStyle(fontWeight: FontWeight.w500), ), ), Expanded(child: Text(value)), ], ), ); } String _formatDate(DateTime date) { return '${date.toLocal()}'.split('.')[0]; } @override void dispose() { _authSubscription?.cancel(); super.dispose(); } } class SessionInfo { final String userId; final String email; final DateTime createdAt; final DateTime lastActivity; final bool isActive; SessionInfo({ required this.userId, required this.email, required this.createdAt, required this.lastActivity, required this.isActive, }); } ``` ## Token Management ### Access Token Information ```dart class TokenManager { final InstantDB db; TokenManager(this.db); String? get currentToken => db.auth.authToken; bool get hasValidToken => db.auth.isAuthenticated; // Check if token needs refresh (if available) bool get needsRefresh { final user = db.auth.currentUser.value; if (user?.refreshToken == null) return false; // Implement your token expiry logic here // This is a placeholder - actual implementation depends on token format return false; } Future refreshTokenIfNeeded() async { if (needsRefresh) { try { await db.auth.refreshUser(); } catch (e) { print('Token refresh failed: $e'); // Handle refresh failure (e.g., redirect to login) } } } } ``` ### Token-based Authentication Sign in with existing tokens: ```dart class TokenAuth extends StatefulWidget { @override State createState() => _TokenAuthState(); } class _TokenAuthState extends State { final _tokenController = TextEditingController(); bool _isLoading = false; String? _errorMessage; Future _signInWithToken() async { setState(() { _isLoading = true; _errorMessage = null; }); try { final db = InstantProvider.of(context); final user = await db.auth.signInWithToken(_tokenController.text.trim()); print('User authenticated with token: ${user.email}'); // Navigation handled by AuthBuilder } on InstantException catch (e) { setState(() { _errorMessage = e.message; }); } finally { setState(() { _isLoading = false; }); } } @override Widget build(BuildContext context) { return Column( children: [ const Text( 'Sign In with Token', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), TextField( controller: _tokenController, decoration: const InputDecoration( labelText: 'Authentication Token', border: OutlineInputBorder(), hintText: 'Paste your token here', ), maxLines: 3, ), const SizedBox(height: 16), if (_errorMessage != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( _errorMessage!, style: TextStyle(color: Colors.red.shade700), ), ), const SizedBox(height: 16), ], SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _signInWithToken, child: _isLoading ? const CircularProgressIndicator() : const Text('Sign In'), ), ), ], ); } } ``` ## Session Persistence ### Custom Session Storage Extend session persistence with custom storage: ```dart import 'package:shared_preferences/shared_preferences.dart'; class SessionStorage { static const String _tokenKey = 'instantdb_auth_token'; static const String _userKey = 'instantdb_user_data'; static Future saveSession(AuthUser user, String token) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_tokenKey, token); await prefs.setString(_userKey, jsonEncode(user.toJson())); } static Future loadSession() async { final prefs = await SharedPreferences.getInstance(); final token = prefs.getString(_tokenKey); final userData = prefs.getString(_userKey); if (token != null && userData != null) { try { final userJson = jsonDecode(userData) as Map; final user = AuthUser.fromJson(userJson); return SessionData(user: user, token: token); } catch (e) { print('Failed to load session: $e'); await clearSession(); } } return null; } static Future clearSession() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_tokenKey); await prefs.remove(_userKey); } } class SessionData { final AuthUser user; final String token; SessionData({required this.user, required this.token}); } ``` ### Session Restoration Restore sessions on app startup: ```dart class SessionRestorationService { final InstantDB db; SessionRestorationService(this.db); Future restoreSession() async { try { final sessionData = await SessionStorage.loadSession(); if (sessionData != null) { // Verify token is still valid final user = await db.auth.signInWithToken(sessionData.token); print('Session restored for: ${user.email}'); return true; } } catch (e) { print('Session restoration failed: $e'); await SessionStorage.clearSession(); } return false; } } // Usage in main app class MyApp extends StatefulWidget { @override State createState() => _MyAppState(); } class _MyAppState extends State { bool _isRestoringSession = true; late final InstantDB db; @override void initState() { super.initState(); _initializeApp(); } Future _initializeApp() async { // Initialize InstantDB db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig(syncEnabled: true), ); // Try to restore session final sessionService = SessionRestorationService(db); await sessionService.restoreSession(); setState(() { _isRestoringSession = false; }); } @override Widget build(BuildContext context) { if (_isRestoringSession) { return MaterialApp( home: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Restoring session...'), ], ), ), ), ); } return InstantProvider( db: db, child: MaterialApp( home: AuthBuilder( builder: (context, user) => user != null ? MainApp(user: user) : AuthScreen(), ), ), ); } } ``` ## Session Security ### Automatic Session Timeout Implement session timeout for security: ```dart class SessionTimeoutManager { final InstantDB db; final Duration timeout; Timer? _timeoutTimer; DateTime _lastActivity = DateTime.now(); SessionTimeoutManager({ required this.db, this.timeout = const Duration(minutes: 30), }); void startMonitoring() { _resetTimer(); } void recordActivity() { _lastActivity = DateTime.now(); _resetTimer(); } void _resetTimer() { _timeoutTimer?.cancel(); _timeoutTimer = Timer(timeout, _handleTimeout); } void _handleTimeout() { final timeSinceLastActivity = DateTime.now().difference(_lastActivity); if (timeSinceLastActivity >= timeout) { // Session has timed out db.auth.signOut(); // Show timeout dialog _showTimeoutDialog(); } else { // Reset timer for remaining time final remainingTime = timeout - timeSinceLastActivity; _timeoutTimer = Timer(remainingTime, _handleTimeout); } } void _showTimeoutDialog() { // Implementation depends on your navigation setup print('Session timed out'); } void dispose() { _timeoutTimer?.cancel(); } } // Usage in app class SessionAwareWidget extends StatefulWidget { final Widget child; const SessionAwareWidget({super.key, required this.child}); @override State createState() => _SessionAwareWidgetState(); } class _SessionAwareWidgetState extends State { SessionTimeoutManager? _timeoutManager; @override void initState() { super.initState(); final db = InstantProvider.of(context); _timeoutManager = SessionTimeoutManager(db: db); _timeoutManager?.startMonitoring(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () => _timeoutManager?.recordActivity(), onPanDown: (_) => _timeoutManager?.recordActivity(), child: widget.child, ); } @override void dispose() { _timeoutManager?.dispose(); super.dispose(); } } ``` ### Secure Session Data Protect sensitive session information: ```dart import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SecureSessionStorage { static const _storage = FlutterSecureStorage( aOptions: AndroidOptions( encryptedSharedPreferences: true, ), iOptions: IOSOptions( accessibility: IOSAccessibility.first_unlock_this_device, ), ); static Future saveToken(String token) async { await _storage.write(key: 'auth_token', value: token); } static Future getToken() async { return await _storage.read(key: 'auth_token'); } static Future saveUserData(Map userData) async { await _storage.write( key: 'user_data', value: jsonEncode(userData), ); } static Future?> getUserData() async { final data = await _storage.read(key: 'user_data'); if (data != null) { return jsonDecode(data) as Map; } return null; } static Future clearAll() async { await _storage.deleteAll(); } } ``` ## Session Analytics Track session metrics for insights: ```dart class SessionAnalytics { final Map _metrics = {}; void trackSessionStart(String userId) { _metrics['session_start'] = DateTime.now().millisecondsSinceEpoch; _metrics['user_id'] = userId; } void trackSessionEnd() { final sessionStart = _metrics['session_start'] as int?; if (sessionStart != null) { final duration = DateTime.now().millisecondsSinceEpoch - sessionStart; _metrics['session_duration'] = duration; } _sendAnalytics(); } void trackUserActivity(String action) { _metrics['last_activity'] = DateTime.now().millisecondsSinceEpoch; _metrics['last_action'] = action; } Future _sendAnalytics() async { // Send metrics to your analytics service print('Session metrics: $_metrics'); // Example: Send to Firebase Analytics, Mixpanel, etc. // await FirebaseAnalytics.instance.logEvent( // name: 'session_end', // parameters: _metrics, // ); } } // Integration with authentication class AnalyticsAwareAuth extends StatefulWidget { @override State createState() => _AnalyticsAwareAuthState(); } class _AnalyticsAwareAuthState extends State { final _analytics = SessionAnalytics(); StreamSubscription? _authSubscription; @override void initState() { super.initState(); _monitorAuthState(); } void _monitorAuthState() { final db = InstantProvider.of(context); _authSubscription = db.auth.onAuthStateChange.listen((user) { if (user != null) { _analytics.trackSessionStart(user.id); } else { _analytics.trackSessionEnd(); } }); } @override Widget build(BuildContext context) { return AuthBuilder( builder: (context, user) { if (user != null) { return MainApp(user: user); } else { return LoginScreen(); } }, ); } @override void dispose() { _authSubscription?.cancel(); super.dispose(); } } ``` ## Best Practices ### 1. Handle Session Expiration Always handle expired sessions gracefully: ```dart class SessionExpirationHandler { static void handleExpiredSession(BuildContext context) { showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Text('Session Expired'), content: const Text('Your session has expired. Please sign in again.'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); // Navigate to login }, child: const Text('Sign In'), ), ], ), ); } } ``` ### 2. Implement Proper Cleanup Clean up sessions on sign out: ```dart Future signOutAndCleanup() async { final db = InstantProvider.of(context); // Sign out from InstantDB await db.auth.signOut(); // Clear stored session data await SessionStorage.clearSession(); // Clear sensitive data await SecureSessionStorage.clearAll(); // Reset app state if needed // Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false); } ``` ### 3. Monitor Network Connectivity Handle offline sessions: ```dart class OfflineSessionHandler { static bool _isOffline = false; static void handleNetworkChange(bool isOnline) { if (!isOnline && !_isOffline) { _isOffline = true; // Cache current session state _cacheSessionState(); } else if (isOnline && _isOffline) { _isOffline = false; // Validate cached session _validateCachedSession(); } } static Future _cacheSessionState() async { // Implementation for caching session } static Future _validateCachedSession() async { // Implementation for validating cached session } } ``` ### 4. Security Considerations Follow security best practices: ```dart class SessionSecurity { // Validate session integrity static bool validateSessionIntegrity(AuthUser user, String token) { // Implement your validation logic return user.id.isNotEmpty && token.isNotEmpty; } // Detect session tampering static bool detectTampering(String storedHash, String currentData) { // Implement tampering detection return storedHash == _generateHash(currentData); } static String _generateHash(String data) { // Generate hash for data integrity return data.hashCode.toString(); } } ``` ## Next Steps Learn more about authentication and session features: - [User Management](/docs/auth/users) - User authentication methods - [Permissions](/docs/auth/permissions) - Role-based access control - [Offline Authentication](/docs/advanced/offline) - Handling auth when offline - [Security Best Practices](/docs/advanced/troubleshooting) - Advanced security considerations --- # User Management > Authentication and user management with Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/auth/users InstantDB provides comprehensive user authentication with email/password, magic links, magic codes, and session management. All authentication methods integrate seamlessly with real-time sync and presence features. ## Getting Started with Auth ### Initialize with Authentication ```dart final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, ), ); // Listen to authentication state changes db.auth.onAuthStateChange.listen((user) { if (user != null) { print('User signed in: ${user.email}'); } else { print('User signed out'); } }); ``` ### Check Authentication Status ```dart // One-time check final currentUser = db.getAuth(); // Reactive updates final authSignal = db.subscribeAuth(); // Use in widgets Watch((context) { final user = db.subscribeAuth().value; return user != null ? WelcomeScreen(user: user) : LoginScreen(); }); ``` ## Authentication Methods ### Magic Codes One-time password (OTP) authentication: ```dart class MagicCodeAuth extends StatefulWidget { @override State createState() => _MagicCodeAuthState(); } class _MagicCodeAuthState extends State { final _emailController = TextEditingController(); final _codeController = TextEditingController(); bool _isLoading = false; bool _codeSent = false; String? _errorMessage; Future _sendMagicCode() async { setState(() { _isLoading = true; _errorMessage = null; }); try { final db = InstantProvider.of(context); await db.auth.sendMagicCode(_emailController.text.trim()); setState(() { _codeSent = true; }); } on InstantException catch (e) { setState(() { _errorMessage = e.message; }); } finally { setState(() { _isLoading = false; }); } } Future _verifyCode() async { setState(() { _isLoading = true; _errorMessage = null; }); try { final db = InstantProvider.of(context); final user = await db.auth.verifyMagicCode( email: _emailController.text.trim(), code: _codeController.text.trim(), ); print('User authenticated: ${user.email}'); // Navigation handled by AuthBuilder } on InstantException catch (e) { setState(() { _errorMessage = e.message; }); } finally { setState(() { _isLoading = false; }); } } @override Widget build(BuildContext context) { return Column( children: [ TextField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email Address', keyboardType: TextInputType.emailAddress, ), enabled: !_codeSent, ), const SizedBox(height: 16), if (_codeSent) ...[ TextField( controller: _codeController, decoration: const InputDecoration( labelText: 'Verification Code', hintText: 'Enter 6-digit code', ), keyboardType: TextInputType.number, maxLength: 6, ), const SizedBox(height: 16), ], if (_errorMessage != null) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( _errorMessage!, style: TextStyle(color: Colors.red.shade700), ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : (_codeSent ? _verifyCode : _sendMagicCode), child: _isLoading ? const CircularProgressIndicator() : Text(_codeSent ? 'Verify Code' : 'Send Code'), ), ), if (_codeSent) ...[ const SizedBox(height: 16), OutlinedButton( onPressed: () { setState(() { _codeSent = false; _codeController.clear(); }); }, child: const Text('Use Different Email'), ), ], ], ); } } ``` ## Complete Authentication Flow Combine all authentication methods in a unified experience: ```dart class AuthFlow extends StatefulWidget { @override State createState() => _AuthFlowState(); } class _AuthFlowState extends State { AuthMethod _currentMethod = AuthMethod.emailPassword; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Sign In'), ), body: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ // Method selector SegmentedButton( segments: const [ ButtonSegment( value: AuthMethod.emailPassword, label: Text('Email/Password'), icon: Icon(Icons.password), ), ButtonSegment( value: AuthMethod.magicLink, label: Text('Magic Link'), icon: Icon(Icons.link), ), ButtonSegment( value: AuthMethod.magicCode, label: Text('Magic Code'), icon: Icon(Icons.pin), ), ], selected: {_currentMethod}, onSelectionChanged: (selection) { setState(() { _currentMethod = selection.first; }); }, ), const SizedBox(height: 32), // Authentication method Expanded( child: switch (_currentMethod) { AuthMethod.emailPassword => EmailPasswordAuth(), AuthMethod.magicLink => MagicLinkAuth(), AuthMethod.magicCode => MagicCodeAuth(), }, ), ], ), ), ); } } enum AuthMethod { emailPassword, magicLink, magicCode, } ``` ## Reactive Authentication Widget Use the AuthBuilder widget for reactive authentication UI: ```dart class App extends StatelessWidget { @override Widget build(BuildContext context) { return InstantProvider( db: db, child: AuthBuilder( builder: (context, user) { if (user != null) { // User is authenticated return MainApp(user: user); } else { // User needs to sign in return AuthFlow(); } }, loadingBuilder: (context) { return const Scaffold( body: Center( child: CircularProgressIndicator(), ), ); }, ), ); } } class MainApp extends StatelessWidget { final AuthUser user; const MainApp({super.key, required this.user}); @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Scaffold( appBar: AppBar( title: Text('Welcome, ${user.email}'), actions: [ PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( child: const Text('Profile'), onTap: () => _showProfile(context), ), PopupMenuItem( child: const Text('Sign Out'), onTap: () => db.auth.signOut(), ), ], ), ], ), body: YourAppContent(), ); } void _showProfile(BuildContext context) { showDialog( context: context, builder: (context) => Dialog( child: Padding( padding: const EdgeInsets.all(24), child: UserProfile(user: user), ), ), ); } } ``` ## OAuth Sign-In InstantDB supports OAuth providers (Google, Apple, GitHub, LinkedIn, Clerk, Firebase) through either the redirect flow or provider-specific ID token helpers. ### Redirect Flow Build an authorization URL, open it, and exchange the returned `code` for a session. PKCE (S256) is enabled by default. ```dart // 1. Build the authorization URL final flow = db.auth.createAuthorizationUrl( clientName: 'google', // OAuth client name from the InstantDB dashboard redirectUri: 'myapp://oauth', // must match an allowed redirect for the client scopes: ['email', 'profile'], // optional ); // 2. Open flow.url (url_launcher, flutter_web_auth_2, in-app webview, ...) // On redirect you receive ?code=... // 3. Exchange the code for a session await db.auth.exchangeCodeForToken( code: code, codeVerifier: flow.codeVerifier, ); ``` `createAuthorizationUrl` returns an `OAuthFlow`: ```dart class OAuthFlow { final String url; final String? codeVerifier; // pass back to exchangeCodeForToken final String? state; } ``` ### Provider ID Token Helpers If you already have an ID token from a provider's native SDK (e.g. `google_sign_in`, `sign_in_with_apple`), pass it directly. Each helper wraps `signInWithIdToken` and takes the dashboard `clientName`. ```dart await db.auth.signInWithGoogle(idToken: googleIdToken); // clientName defaults to 'google' await db.auth.signInWithApple(idToken: appleIdToken); // clientName defaults to 'apple' await db.auth.signInWithClerk(idToken: clerkToken, clientName: 'clerk'); await db.auth.signInWithFirebase(idToken: firebaseToken, clientName: 'firebase'); ``` Google and Apple also accept an optional `nonce`. ### OAuthButton Widget `OAuthButton` is a drop-in sign-in button. It builds the authorization URL and hands the `OAuthFlow` to your `onLaunch` callback — your app opens the URL (it does **not** bundle `url_launcher`) and calls `exchangeCodeForToken` on return. ```dart OAuthButton( provider: OAuthProvider.google, clientName: 'google', redirectUri: 'myapp://oauth', onLaunch: (flow) async { final result = await FlutterWebAuth2.authenticate( url: flow.url, callbackUrlScheme: 'myapp', ); final code = Uri.parse(result).queryParameters['code']!; await db.auth.exchangeCodeForToken( code: code, codeVerifier: flow.codeVerifier, ); }, ) ``` `OAuthProvider` supplies a default label, color, and icon for `google`, `apple`, `github`, `linkedin`, `clerk`, and `firebase`. Override with `label`, `color`, or `icon`, and pass `scopes` or `usePKCE: false` as needed. ## Best Practices ### 1. Handle Authentication States Always provide loading and error states: ```dart AuthBuilder( builder: (context, user) => user != null ? MainApp() : LoginScreen(), loadingBuilder: (context) => LoadingScreen(), errorBuilder: (context, error) => ErrorScreen(error: error), ) ``` ### 2. Validate Input Validate email and password formats: ```dart String? validateEmail(String email) { if (email.isEmpty) return 'Email is required'; if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(email)) { return 'Please enter a valid email'; } return null; } String? validatePassword(String password) { if (password.isEmpty) return 'Password is required'; if (password.length < 8) return 'Password must be at least 8 characters'; return null; } ``` ### 3. Secure Token Storage InstantDB automatically handles secure token storage, but you can access tokens if needed: ```dart final authToken = db.auth.authToken; if (authToken != null) { // Token is available for API calls } ``` ### 4. Handle Authentication Errors Provide clear error messages: ```dart void handleAuthError(InstantException error) { String userMessage; switch (error.code) { case 'invalid_email': userMessage = 'Please enter a valid email address'; break; case 'weak_password': userMessage = 'Password is too weak. Please use a stronger password'; break; case 'auth_error': userMessage = 'Authentication failed. Please try again'; break; default: userMessage = 'An unexpected error occurred'; } showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Authentication Error'), content: Text(userMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('OK'), ), ], ), ); } ``` ## Next Steps Learn more about authentication features: - [Session Management](/docs/auth/sessions) - Managing user sessions and tokens - [Permissions](/docs/auth/permissions) - Role-based access control - [User Presence](/docs/realtime/presence) - Adding users to collaborative features - [Advanced Auth](/docs/advanced/offline) - Offline authentication handling --- # Database Initialization > How to initialize and configure InstantDB Source: https://flutter-instantdb.vercel.app/docs/concepts/database The `InstantDB` class is the main entry point for your real-time database. It manages local storage, real-time synchronization, and provides the query and mutation APIs. ## Basic Initialization The simplest way to initialize InstantDB: ```dart final db = await InstantDB.init( appId: 'your-app-id', ); ``` ## Configuration Options Customize your database behavior with `InstantConfig`: ```dart final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, // Enable real-time sync persistenceDir: 'my_app_db', // Custom database directory verboseLogging: true , // Logging level baseUrl: 'https://api.instantdb.com', // Custom API endpoint ), ); ``` ### Configuration Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `syncEnabled` | `bool` | `true` | Enable/disable real-time synchronization | | `persistenceDir` | `String?` | `null` | Custom directory for local database files | | `logLevel` | `LogLevel` | `LogLevel.warning` | Logging verbosity level | | `baseUrl` | `String` | `'https://api.instantdb.com'` | InstantDB API endpoint | | `websocketUrl` | `String?` | `null` | Custom WebSocket endpoint for real-time sync | ## Application Integration ### Using InstantProvider The recommended way to provide your database instance throughout your app: ```dart class MyApp extends StatelessWidget { final InstantDB db; const MyApp({super.key, required this.db}); @override Widget build(BuildContext context) { return InstantProvider( db: db, child: MaterialApp( title: 'My App', home: const HomePage(), ), ); } } ``` ### Accessing the Database Access your database instance from any widget: ```dart class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); // Use db for queries and mutations return Container(); } } ``` ## Lifecycle Management ### Initialization Status Check if your database is ready: ```dart // Reactive signal that tracks initialization final isReady = db.isReady; // Use in widgets Watch((context) { if (!db.isReady.value) { return const CircularProgressIndicator(); } return MyMainWidget(); }); ``` ### Disposal Clean up resources when your app closes: ```dart @override void dispose() { db.dispose(); // Clean up database resources super.dispose(); } ``` ## Error Handling Handle initialization errors gracefully: ```dart try { final db = await InstantDB.init(appId: 'your-app-id'); print('Database initialized successfully'); } on InstantException catch (e) { print('Failed to initialize database: ${e.message}'); // Handle error appropriately } ``` ## Development vs Production ### Development Setup For development, enable verbose logging: ```dart final db = await InstantDB.init( appId: 'dev-app-id', config: const InstantConfig( logLevel: LogLevel.debug, syncEnabled: true, ), ); ``` ### Production Setup For production, optimize for performance: ```dart final db = await InstantDB.init( appId: 'prod-app-id', config: const InstantConfig( logLevel: LogLevel.warning, syncEnabled: true, ), ); ``` ## Multiple Database Instances You can create multiple database instances for different purposes: ```dart // Main app database final appDb = await InstantDB.init(appId: 'main-app-id'); // Analytics database final analyticsDb = await InstantDB.init( appId: 'analytics-app-id', config: const InstantConfig( persistenceDir: 'analytics_db', ), ); ``` ## Platform Considerations ### Web Platform On web, InstantDB uses IndexedDB for local storage: ```dart // No special configuration needed for web final db = await InstantDB.init(appId: 'your-app-id'); ``` ### Mobile Platforms On mobile, InstantDB uses SQLite: ```dart // Optional: Specify custom database directory final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( persistenceDir: 'my_mobile_db', ), ); ``` ### Desktop Platforms Desktop platforms use SQLite with full filesystem access: ```dart import 'dart:io'; final db = await InstantDB.init( appId: 'your-app-id', config: InstantConfig( persistenceDir: Platform.isWindows ? 'my_windows_db' : 'my_desktop_db', ), ); ``` ## Next Steps Now that your database is initialized, learn about: - [Schema Definition](/docs/concepts/schema) - Structure your data - [Queries](/docs/queries/basics) - Fetch data reactively - [Mutations](/docs/concepts/transactions) - Create, update, and delete data - [Real-time Sync](/docs/realtime/sync) - Handle live updates --- # Schema Definition > Define and validate data structures with Flutter InstantDB schemas Source: https://flutter-instantdb.vercel.app/docs/concepts/schema Flutter InstantDB provides a powerful schema system for defining, validating, and enforcing data structures. Schemas ensure data consistency, provide type safety, and catch errors early in development. ## Why Use Schemas? Schemas provide several key benefits: - ✅ **Type Safety** - Catch type errors at development time - ✅ **Data Validation** - Ensure data meets your requirements - ✅ **Documentation** - Schema serves as living documentation - ✅ **Auto-completion** - Better IDE support with defined structures - ✅ **Runtime Protection** - Prevent invalid data from entering your database ## Basic Schema Definition ### Simple Field Types Define schemas using the `Schema` class with various field types: ```dart import 'package:flutter_instantdb/flutter_instantdb.dart'; // String field final nameSchema = Schema.string( minLength: 1, maxLength: 100, ); // Number field final ageSchema = Schema.number( min: 0, max: 150, ); // Boolean field final activeSchema = Schema.boolean(); // Email field with built-in validation final emailSchema = Schema.email(); // URL field final websiteSchema = Schema.url(); // UUID field final idSchema = Schema.id(); ``` ### Object Schemas Combine fields into object schemas: ```dart final userSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string(minLength: 1, maxLength: 100), 'email': Schema.email(), 'age': Schema.number(min: 0, max: 150), 'active': Schema.boolean(), 'website': Schema.url().optional(), // Optional field }, required: ['id', 'name', 'email']); // Specify required fields ``` ### Array Schemas Define arrays of specific types: ```dart // Array of strings final tagsSchema = Schema.array(Schema.string()); // Array of objects final commentsSchema = Schema.array( Schema.object({ 'id': Schema.id(), 'text': Schema.string(minLength: 1), 'authorId': Schema.id(), 'createdAt': Schema.number(), }) ); // Array with size constraints final skillsSchema = Schema.array( Schema.string(), minLength: 1, // At least one skill maxLength: 10, // At most 10 skills ); ``` ## Schema Validation ### Validating Data Use schemas to validate data before storing: ```dart final todoSchema = Schema.object({ 'id': Schema.id(), 'text': Schema.string(minLength: 1, maxLength: 500), 'completed': Schema.boolean(), 'priority': Schema.string().oneOf(['low', 'medium', 'high']), 'createdAt': Schema.number(), 'tags': Schema.array(Schema.string()).optional(), }); // Validate data final todoData = { 'id': db.id(), 'text': 'Learn InstantDB schemas', 'completed': false, 'priority': 'medium', 'createdAt': DateTime.now().millisecondsSinceEpoch, 'tags': ['flutter', 'database'], }; // Check if data is valid final isValid = todoSchema.validate(todoData); print('Valid: $isValid'); // true // Get validation errors final errors = todoSchema.getErrors(todoData); if (errors.isNotEmpty) { print('Validation errors: $errors'); } ``` ### Validation in Practice Create a validation helper for your entities: ```dart class TodoValidator { static final schema = Schema.object({ 'id': Schema.id(), 'text': Schema.string(minLength: 1, maxLength: 500), 'completed': Schema.boolean(), 'priority': Schema.string().oneOf(['low', 'medium', 'high']), 'createdAt': Schema.number(), 'userId': Schema.id(), 'tags': Schema.array(Schema.string()).optional(), }, required: ['id', 'text', 'completed', 'priority', 'createdAt', 'userId']); static ValidationResult validate(Map data) { final isValid = schema.validate(data); final errors = schema.getErrors(data); return ValidationResult( isValid: isValid, errors: errors, ); } static Map sanitize(Map data) { // Remove fields not in schema final validKeys = schema.properties.keys.toSet(); final sanitized = {}; data.forEach((key, value) { if (validKeys.contains(key)) { sanitized[key] = value; } }); return sanitized; } } class ValidationResult { final bool isValid; final List errors; const ValidationResult({ required this.isValid, required this.errors, }); } // Usage Future createTodoSafely(Map todoData) async { final validation = TodoValidator.validate(todoData); if (!validation.isValid) { throw InstantException( message: 'Invalid todo data: ${validation.errors.join(', ')}', code: 'validation_error', ); } final sanitizedData = TodoValidator.sanitize(todoData); await db.transact([ ...db.create('todos', sanitizedData), ]); } ``` ## Entity Schema Builder For complex applications, use the `InstantSchemaBuilder` to define schemas for multiple entity types: ```dart final userSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string(minLength: 1, maxLength: 100), 'email': Schema.email(), 'role': Schema.string().oneOf(['user', 'admin', 'moderator']), 'createdAt': Schema.number(), 'profile': Schema.object({ 'bio': Schema.string(maxLength: 500).optional(), 'avatar': Schema.url().optional(), 'preferences': Schema.object({ 'theme': Schema.string().oneOf(['light', 'dark', 'auto']), 'notifications': Schema.boolean(), }), }).optional(), }); final postSchema = Schema.object({ 'id': Schema.id(), 'title': Schema.string(minLength: 1, maxLength: 200), 'content': Schema.string(minLength: 1), 'authorId': Schema.id(), 'published': Schema.boolean(), 'publishedAt': Schema.number().optional(), 'tags': Schema.array(Schema.string()), 'metadata': Schema.object({ 'viewCount': Schema.number(min: 0), 'likeCount': Schema.number(min: 0), 'featured': Schema.boolean(), }), }); // Build complete schema final appSchema = InstantSchemaBuilder() .addEntity('users', userSchema) .addEntity('posts', postSchema) .addEntity('comments', Schema.object({ 'id': Schema.id(), 'postId': Schema.id(), 'authorId': Schema.id(), 'text': Schema.string(minLength: 1, maxLength: 1000), 'createdAt': Schema.number(), 'approved': Schema.boolean(), })) .build(); // Use schema with database final db = await InstantDB.init( appId: 'your-app-id', schema: appSchema, // Optional: Apply schema validation ); ``` ## Advanced Schema Patterns ### Conditional Validation Create schemas with conditional validation: ```dart final eventSchema = Schema.object({ 'id': Schema.id(), 'type': Schema.string().oneOf(['meeting', 'deadline', 'reminder']), 'title': Schema.string(minLength: 1), 'date': Schema.number(), // Conditional fields based on type 'meetingDetails': Schema.object({ 'location': Schema.string(), 'attendees': Schema.array(Schema.id()), }).when('type', 'meeting'), // Only required when type is 'meeting' 'reminderDetails': Schema.object({ 'reminderTime': Schema.number(), 'recurring': Schema.boolean(), }).when('type', 'reminder'), }); ``` ### Custom Validation Define custom validation logic: ```dart final passwordSchema = Schema.string() .custom((value) { if (value.length < 8) { return 'Password must be at least 8 characters'; } if (!RegExp(r'[A-Z]').hasMatch(value)) { return 'Password must contain uppercase letter'; } if (!RegExp(r'[a-z]').hasMatch(value)) { return 'Password must contain lowercase letter'; } if (!RegExp(r'[0-9]').hasMatch(value)) { return 'Password must contain number'; } if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(value)) { return 'Password must contain special character'; } return null; // Valid }); final userRegistrationSchema = Schema.object({ 'email': Schema.email(), 'password': passwordSchema, 'confirmPassword': Schema.string(), }).custom((data) { if (data['password'] != data['confirmPassword']) { return 'Passwords do not match'; } return null; }); ``` ### Nested Object Validation Handle complex nested structures: ```dart final organizationSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string(minLength: 1), 'settings': Schema.object({ 'billing': Schema.object({ 'plan': Schema.string().oneOf(['free', 'pro', 'enterprise']), 'subscriptionId': Schema.string().optional(), 'trialEndsAt': Schema.number().optional(), }), 'features': Schema.object({ 'maxUsers': Schema.number(min: 1), 'storageLimit': Schema.number(min: 0), // in GB 'apiAccess': Schema.boolean(), 'integrations': Schema.array(Schema.string()), }), 'security': Schema.object({ 'twoFactorRequired': Schema.boolean(), 'passwordPolicy': Schema.object({ 'minLength': Schema.number(min: 8, max: 128), 'requireUppercase': Schema.boolean(), 'requireNumbers': Schema.boolean(), 'requireSymbols': Schema.boolean(), }), 'sessionTimeout': Schema.number(min: 300, max: 86400), // 5 min to 24 hours }), }), }); ``` ## Schema Integration ### With Queries Schemas help ensure query results match expected structures: ```dart final userQuerySchema = Schema.object({ 'users': Schema.array(userSchema), }); Future> getUsers() async { final result = await db.queryOnce({'users': {}}); // Validate query result if (!userQuerySchema.validate(result.data)) { throw InstantException( message: 'Invalid query result structure', code: 'schema_error', ); } return (result.data!['users'] as List) .map((json) => User.fromJson(json)) .toList(); } ``` ### With Transactions Validate data before transactions: ```dart class SchemaAwareService { final InstantDB db; final Schema schema; SchemaAwareService(this.db, this.schema); Future create(String entityType, Map data) async { // Validate against schema final entitySchema = schema.getEntity(entityType); if (entitySchema != null && !entitySchema.validate(data)) { final errors = entitySchema.getErrors(data); throw InstantException( message: 'Schema validation failed: ${errors.join(', ')}', code: 'validation_error', ); } await db.transact([ ...db.create(entityType, data), ]); } Future update(String entityId, Map data) async { // Validate partial update data final updates = _validatePartialUpdate(data); await db.transact([ db.update(entityId, updates), ]); } Map _validatePartialUpdate(Map data) { // Custom validation logic for partial updates final validated = {}; data.forEach((key, value) { if (_isValidField(key, value)) { validated[key] = value; } }); return validated; } bool _isValidField(String key, dynamic value) { // Implement field-level validation return true; } } ``` ## Schema Versioning Handle schema evolution over time: ```dart class SchemaVersionManager { static const int currentVersion = 2; static Schema getSchemaForVersion(int version) { switch (version) { case 1: return _getV1Schema(); case 2: return _getV2Schema(); default: throw ArgumentError('Unsupported schema version: $version'); } } static Schema _getV1Schema() { return Schema.object({ 'id': Schema.id(), 'name': Schema.string(), 'email': Schema.email(), }); } static Schema _getV2Schema() { return Schema.object({ 'id': Schema.id(), 'name': Schema.string(), 'email': Schema.email(), 'profile': Schema.object({ 'bio': Schema.string().optional(), 'avatar': Schema.url().optional(), }).optional(), 'version': Schema.number(), }); } static Map migrateData( Map data, int fromVersion, int toVersion, ) { var migrated = Map.from(data); for (int v = fromVersion; v < toVersion; v++) { migrated = _migrateBetweenVersions(migrated, v, v + 1); } return migrated; } static Map _migrateBetweenVersions( Map data, int from, int to, ) { switch ('$from->$to') { case '1->2': return { ...data, 'profile': null, 'version': 2, }; default: return data; } } } ``` ## Best Practices ### 1. Start Simple Begin with basic schemas and add complexity as needed: ```dart // ✅ Good: Start simple final userSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string(), 'email': Schema.email(), }); // ❌ Avoid: Over-engineering from the start final userSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string().custom(...).transform(...), 'email': Schema.email().custom(...).when(...), // ... many complex validations }); ``` ### 2. Use Meaningful Constraints Apply constraints that reflect real business rules: ```dart // ✅ Good: Meaningful constraints final productSchema = Schema.object({ 'name': Schema.string(minLength: 1, maxLength: 100), 'price': Schema.number(min: 0.01, max: 999999.99), 'category': Schema.string().oneOf(['electronics', 'clothing', 'books']), 'inStock': Schema.boolean(), }); // ❌ Avoid: Arbitrary or missing constraints final productSchema = Schema.object({ 'name': Schema.string(), // No length limits 'price': Schema.number(), // Could be negative 'category': Schema.string(), // Any string allowed }); ``` ### 3. Document Your Schemas Add comments and documentation: ```dart /// User entity schema /// Represents a registered user in the system final userSchema = Schema.object({ 'id': Schema.id(), // UUID generated by InstantDB 'name': Schema.string(minLength: 1, maxLength: 100), // Display name 'email': Schema.email(), // Must be valid email address 'role': Schema.string().oneOf(['user', 'admin']), // User permission level 'createdAt': Schema.number(), // Unix timestamp 'lastLoginAt': Schema.number().optional(), // Unix timestamp, null if never logged in }); ``` ### 4. Validate Early and Often Validate data at multiple points: ```dart class DataService { // Validate on input Future createUser(Map userData) async { _validateUserData(userData); await db.transact([ ...db.create('users', userData), ]); } // Validate on output Future getUser(String userId) async { final result = await db.queryOnce({ 'users': {'where': {'id': userId}}, }); final userData = result.data?['users']?.first; if (userData != null) { _validateUserData(userData); // Ensure data integrity return User.fromJson(userData); } throw Exception('User not found'); } void _validateUserData(Map data) { if (!userSchema.validate(data)) { throw InstantException( message: 'Invalid user data', code: 'validation_error', ); } } } ``` ### 5. Handle Validation Errors Gracefully Provide helpful error messages: ```dart class UserFriendlyValidator { static String formatValidationErrors(List errors) { if (errors.isEmpty) return ''; final formatted = errors.map((error) { // Convert technical errors to user-friendly messages if (error.contains('minLength')) { return 'This field is too short'; } else if (error.contains('email')) { return 'Please enter a valid email address'; } else if (error.contains('required')) { return 'This field is required'; } return error; }).join(', '); return formatted; } } ``` ## Performance Considerations ### Schema Caching Cache compiled schemas for better performance: ```dart class SchemaCache { static final Map _cache = {}; static Schema getOrCreate(String key, Schema Function() factory) { return _cache.putIfAbsent(key, factory); } static void clear() { _cache.clear(); } } // Usage final userSchema = SchemaCache.getOrCreate('user', () { return Schema.object({ 'id': Schema.id(), 'name': Schema.string(), 'email': Schema.email(), }); }); ``` ### Lazy Validation Only validate when necessary: ```dart class LazyValidatedData { final Map _data; final Schema _schema; bool? _isValid; List? _errors; LazyValidatedData(this._data, this._schema); bool get isValid { _isValid ??= _schema.validate(_data); return _isValid!; } List get errors { _errors ??= _schema.getErrors(_data); return _errors!; } Map get data => _data; } ``` ## Next Steps Now that you understand schemas, explore related topics: - [Database Setup](/docs/concepts/database) - Initialize your database with schemas - [Queries](/docs/queries/basics) - Type-safe queries with schema validation - [Transactions](/docs/api/transactions) - Schema-validated mutations - [Advanced Patterns](/docs/advanced/migration) - Schema migration strategies --- # Flutter Widgets > Reactive widgets for Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/flutter/widgets Flutter InstantDB provides specialized widgets that automatically update when your data changes. These widgets handle loading states, errors, and real-time updates seamlessly. ## InstantBuilder The `InstantBuilder` widget is the foundation for reactive UI in Flutter InstantDB. It automatically rebuilds when query results change. ### Basic Usage ```dart InstantBuilder( query: {'todos': {}}, builder: (context, result) { if (result.isLoading) { return const CircularProgressIndicator(); } if (result.hasError) { return Text('Error: ${result.error}'); } final todos = result.data!['todos'] as List; return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return ListTile( title: Text(todo['text']), trailing: Checkbox( value: todo['completed'], onChanged: (value) => _toggleTodo(todo['id'], value), ), ); }, ); }, ) ``` ### With Custom Loading and Error Widgets ```dart InstantBuilder( query: {'todos': {}}, loadingBuilder: (context) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Loading todos...'), ], ), ), errorBuilder: (context, error) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error, size: 64, color: Colors.red), SizedBox(height: 16), Text('Failed to load todos'), SizedBox(height: 8), Text(error, style: TextStyle(fontSize: 12)), SizedBox(height: 16), ElevatedButton( onPressed: () => _retry(), child: Text('Retry'), ), ], ), ), builder: (context, result) { final todos = result.data!['todos'] as List; if (todos.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_circle, size: 64, color: Colors.grey), SizedBox(height: 16), Text('No todos yet'), Text('Add your first todo above!'), ], ), ); } return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile(todo: todos[index]), ); }, ) ``` ## InstantBuilderTyped For better type safety and data transformation, use `InstantBuilderTyped`: ```dart InstantBuilderTyped>( query: {'todos': {}}, transformer: (data) { final todosData = data['todos'] as List; return todosData.map((json) => Todo.fromJson(json)).toList(); }, builder: (context, todos) { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return TodoTile( title: todo.text, isCompleted: todo.completed, onToggle: () => _toggleTodo(todo.id), ); }, ); }, ) ``` ### Advanced Transformer Transform and sort data before rendering: ```dart InstantBuilderTyped>( query: {'todos': {}}, transformer: (data) { final todosData = data['todos'] as List; final todos = todosData.map((json) => Todo.fromJson(json)).toList(); // Sort by priority, then by creation date todos.sort((a, b) { final priorityComparison = b.priority.compareTo(a.priority); if (priorityComparison != 0) return priorityComparison; return b.createdAt.compareTo(a.createdAt); }); return todos; }, builder: (context, sortedTodos) { return ListView.builder( itemCount: sortedTodos.length, itemBuilder: (context, index) => TodoTile(todo: sortedTodos[index]), ); }, ) ``` ## Watch Widget For simpler reactive widgets that don't need query data, use the `Watch` widget: ```dart class CounterDisplay extends StatelessWidget { final Signal counter; const CounterDisplay({super.key, required this.counter}); @override Widget build(BuildContext context) { return Watch((context) { return Text( 'Count: ${counter.value}', style: Theme.of(context).textTheme.headlineMedium, ); }); } } ``` ## InstantProvider The `InstantProvider` widget provides access to your InstantDB instance throughout your widget tree: ```dart class MyApp extends StatelessWidget { final InstantDB db; const MyApp({super.key, required this.db}); @override Widget build(BuildContext context) { return InstantProvider( db: db, child: MaterialApp( title: 'My App', home: const HomePage(), ), ); } } // Access in child widgets class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Scaffold( body: InstantBuilder( query: {'todos': {}}, builder: (context, result) { // Build your UI }, ), floatingActionButton: FloatingActionButton( onPressed: () => _addTodo(db), child: const Icon(Icons.add), ), ); } } ``` ## Custom Reactive Widgets Create your own reactive widgets using the underlying signals: ```dart class TodoCounter extends StatelessWidget { const TodoCounter({super.key}); @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final todosQuery = db.query({'todos': {}}); return Watch((context) { final result = todosQuery.value; if (!result.hasData) { return const Text('...'); } final todos = result.data!['todos'] as List; final completed = todos.where((todo) => todo['completed'] == true).length; return Text('$completed / ${todos.length} completed'); }); } } ``` ## Widget Composition Combine multiple InstantDB widgets for complex UIs: ```dart class TodosPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Todos'), actions: [ // Show todo count in app bar const TodoCounter(), const SizedBox(width: 16), ], ), body: Column( children: [ // Add todo form const AddTodoForm(), // Filter tabs const TodoFilters(), // Main todo list Expanded( child: InstantBuilderTyped>( query: {'todos': {}}, transformer: _transformTodos, builder: (context, todos) { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile( todo: todos[index], onToggle: () => _toggleTodo(todos[index]), onDelete: () => _deleteTodo(todos[index].id), ), ); }, ), ), ], ), ); } } ``` ## Performance Considerations ### Optimize Rebuilds Use `const` constructors when possible: ```dart InstantBuilder( query: {'todos': {}}, loadingBuilder: (context) => const LoadingSpinner(), // const errorBuilder: (context, error) => ErrorWidget(error: error), builder: (context, result) { final todos = result.data!['todos'] as List; return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile( key: ValueKey(todos[index]['id']), // Stable keys todo: todos[index], ), ); }, ) ``` ### Selective Updates Use specific queries to minimize unnecessary rebuilds: ```dart // Instead of querying all todos and filtering in the widget InstantBuilder( query: { 'todos': { 'where': {'userId': currentUserId}, // Filter at query level }, }, builder: (context, result) { // Only rebuilds when user's todos change }, ) ``` ### Memoization Use `useMemo` for expensive computations: ```dart InstantBuilderTyped>( query: {'todos': {}}, transformer: (data) { // This transformer only runs when data actually changes return (data['todos'] as List) .map((json) => Todo.fromJson(json)) .toList(); }, builder: (context, todos) { // Expensive computation memoized final groupedTodos = useMemo( () => _groupTodosByCategory(todos), [todos], ); return CategoryView(groups: groupedTodos); }, ) ``` ## Error Boundaries Handle errors at the widget level: ```dart class TodosWithErrorBoundary extends StatelessWidget { @override Widget build(BuildContext context) { return ErrorBoundary( onError: (error, stackTrace) { // Log error to analytics Analytics.reportError(error, stackTrace); }, child: InstantBuilder( query: {'todos': {}}, builder: (context, result) { if (result.hasError) { return ErrorRetryWidget( error: result.error!, onRetry: () => _retryQuery(), ); } // Success case return TodosList(data: result.data!); }, ), ); } } ``` ## Collaboration Widgets Flutter InstantDB ships reactive widgets that mirror InstantDB's React presence hooks. Each resolves the client via `InstantProvider.of(context)`, joins the room, and rebuilds on change. ```dart // usePresence — rebuilds on peer presence changes PresenceBuilder( roomId: 'doc-42', initialPresence: {'name': 'Alice'}, builder: (context, room, peers) => Text('${peers.length} online'), ) // useTopicEffect — side-effect on ephemeral messages TopicListener( roomId: 'doc-42', topic: 'emoji', onEvent: (data) => _showFloatingEmoji(data['emoji']), child: const Editor(), ) // typing peers TypingIndicatorBuilder( roomId: 'doc-42', builder: (context, typing) => typing.isEmpty ? const SizedBox() : Text('${typing.length} typing…'), ) // live reactions ReactionsBuilder( roomId: 'doc-42', builder: (context, reactions) => Wrap(children: [for (final r in reactions) Text(r.emoji)]), ) // multiplayer cursors — Flutter equivalent of CursorOverlay( roomId: 'doc-42', userName: 'Alice', userColor: '#E91E63', child: const Canvas(), ) ``` See [Presence System](/docs/realtime/presence) for the full reference. ## Next Steps Learn more about Flutter InstantDB widgets and patterns: - [Presence Widgets](/docs/realtime/presence) - Show real-time user presence - [Authentication Widgets](/docs/auth/widgets) - Handle user authentication UI - [Advanced Patterns](/docs/flutter/advanced-patterns) - Complex widget compositions - [Performance Tips](/docs/flutter/performance) - Optimize your reactive UI --- # Installation > How to install Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/getting-started/installation Flutter InstantDB is a real-time, offline-first database client that provides reactive bindings for Flutter applications. It enables you to build collaborative, real-time applications with ease. ## Requirements - Flutter SDK >= 3.8.0 - Dart SDK >= 3.8.0 - An InstantDB app ID (get one at [instantdb.com](https://instantdb.com)) ## Installation Add Flutter InstantDB to your `pubspec.yaml`: ```yaml dependencies: flutter_instantdb: ^0.1.0 ``` Or install from the command line: ```sh flutter pub add flutter_instantdb ``` ```sh dart pub add flutter_instantdb ``` ## Platform Support Flutter InstantDB supports all Flutter platforms: | Platform | Support | Notes | |----------|---------|--------| | 🤖 Android | ✅ | Full support with SQLite storage | | 🍎 iOS | ✅ | Full support with SQLite storage | | 🌐 Web | ✅ | IndexedDB storage with WebSocket sync | | 🖥️ macOS | ✅ | Full support with SQLite storage | | 🪟 Windows | ✅ | Full support with SQLite storage | | 🐧 Linux | ✅ | Full support with SQLite storage | ## Import Add the import to your Dart files: ```dart import 'package:flutter_instantdb/flutter_instantdb.dart'; ``` ## Get Your App ID 1. Visit [instantdb.com](https://instantdb.com) and create an account 2. Create a new app 3. Copy your App ID from the dashboard 4. **Store your App ID in a `.env` file** in your project root: ```bash INSTANTDB_APP_ID=your-app-id-here ``` 5. Use it in your Flutter app initialization ## Next Steps Now that you have InstantDB Flutter installed, let's set up your first database connection: - [Quick Start Guide](/docs/getting-started/quick-start) - Create your first real-time app - [Database Setup](/docs/concepts/database) - Initialize and configure your database - [Schema Definition](/docs/concepts/schema) - Define your data structure --- # Quick Start > Build your first real-time app with InstantDB Flutter Source: https://flutter-instantdb.vercel.app/docs/getting-started/quick-start This guide will help you build a simple todo app with real-time synchronization in just a few minutes. ## 1. Initialize InstantDB First, initialize your InstantDB instance in your app: ```dart import 'package:flutter/material.dart'; import 'package:flutter_instantdb/flutter_instantdb.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize InstantDB final db = await InstantDB.init( appId: 'your-app-id-here', // Get this from instantdb.com config: const InstantConfig( syncEnabled: true, ), ); runApp(MyApp(db: db)); } ``` ## 2. Create Your App Widget Wrap your app with `InstantProvider` to make the database available throughout your widget tree: ```dart class MyApp extends StatelessWidget { final InstantDB db; const MyApp({super.key, required this.db}); @override Widget build(BuildContext context) { return InstantProvider( db: db, child: MaterialApp( title: 'InstantDB Todo', home: const TodoPage(), ), ); } } ``` ## 3. Build Reactive UI Create a page that automatically updates when data changes: ```dart class TodoPage extends StatefulWidget { const TodoPage({super.key}); @override State createState() => _TodoPageState(); } class _TodoPageState extends State { final _controller = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Todos')), body: Column( children: [ // Add todo input Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: TextField( controller: _controller, decoration: const InputDecoration( hintText: 'Add a todo...', ), onSubmitted: _addTodo, ), ), IconButton( onPressed: () => _addTodo(_controller.text), icon: const Icon(Icons.add), ), ], ), ), // Todo list - automatically updates in real-time! Expanded( child: InstantBuilderTyped>>( query: {'todos': {}}, transformer: (data) { final todos = (data['todos'] as List).cast>(); // Sort by creation time todos.sort((a, b) => (b['createdAt'] ?? 0).compareTo(a['createdAt'] ?? 0)); return todos; }, builder: (context, todos) { if (todos.isEmpty) { return const Center( child: Text('No todos yet. Add one above!'), ); } return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return ListTile( title: Text( todo['text'] ?? '', style: TextStyle( decoration: todo['completed'] == true ? TextDecoration.lineThrough : null, ), ), leading: Checkbox( value: todo['completed'] == true, onChanged: (value) => _toggleTodo(todo), ), trailing: IconButton( icon: const Icon(Icons.delete), onPressed: () => _deleteTodo(todo['id']), ), ); }, ); }, ), ), ], ), ); } void _addTodo(String text) { if (text.trim().isEmpty) return; final db = InstantProvider.of(context); final todoId = db.id(); db.transact([ ...db.create('todos', { 'id': todoId, 'text': text.trim(), 'completed': false, 'createdAt': DateTime.now().millisecondsSinceEpoch, }), ]); _controller.clear(); } void _toggleTodo(Map todo) { final db = InstantProvider.of(context); db.transact( db.tx['todos'][todo['id']].update({ 'completed': !(todo['completed'] == true), }), ); } void _deleteTodo(String todoId) { final db = InstantProvider.of(context); db.transact( db.tx['todos'][todoId].delete(), ); } @override void dispose() { _controller.dispose(); super.dispose(); } } ``` ## 4. Test Real-time Sync That's it! Your app now has: - ✅ **Real-time synchronization** - Changes appear instantly across all connected clients - ✅ **Offline support** - Works offline and syncs when connection returns - ✅ **Reactive UI** - Widgets update automatically when data changes - ✅ **Type-safe operations** - Full Dart type safety ## Try It Out 1. Run your app on multiple devices or browser tabs 2. Add, complete, or delete todos on one device 3. Watch them sync in real-time on all other devices! ## What's Next? Now that you have a working real-time app, explore more features: - [Schema Definition](/docs/concepts/schema) - Add type safety and validation - [Authentication](/docs/auth/users) - Add user accounts and permissions - [Presence System](/docs/realtime/presence) - Show who's online - [Advanced Queries](/docs/queries/advanced) - Complex data fetching - [Offline Handling](/docs/advanced/offline) - Handle network states ## Common Issues ### App ID Not Working? Make sure you've created an app at [instantdb.com](https://instantdb.com) and copied the correct App ID. ### Not Syncing? Check your internet connection and ensure `syncEnabled: true` in your config. ### Build Errors? Make sure you're using Flutter SDK >= 3.8.0 and have run `flutter pub get`. Need help? Check out our [troubleshooting guide](/docs/advanced/troubleshooting) or ask in our [Discord community](https://discord.gg/instantdb). --- # Aggregations > Count, sum, average, min, and max over your data with db.count and db.aggregate Source: https://flutter-instantdb.vercel.app/docs/queries/aggregations Flutter InstantDB can compute aggregates server-side instead of fetching every row and reducing on the client. Use `db.count` for the common case, or `db.aggregate` for sums, averages, grouped results, and more. ## Counting `db.count` returns the number of matching records as a `Future`: ```dart // Count all todos final total = await db.count('todos'); // Count with a where clause final remaining = await db.count('todos', where: {'done': false}); ``` ## Aggregate functions `db.aggregate` runs one or more aggregate functions over an entity type: ```dart Future>> aggregate( String entityType, { required Map aggregates, Map? where, List? groupBy, }); ``` The `aggregates` map pairs a function with the field it operates on. Supported functions are `count`, `sum`, `avg`, `min`, and `max`. For `count`, use `'*'` as the field. ```dart // Single summary row: { 'count': 42, 'avg': 2.3 } final summary = await db.aggregate('todos', aggregates: { 'count': '*', 'avg': 'priority', }); final row = summary.first; print('${row['count']} todos, avg priority ${row['avg']}'); ``` ### Filtering Pass `where` to aggregate over a subset: ```dart final highPriority = await db.aggregate('todos', aggregates: {'count': '*'}, where: {'priority': {'\$gte': 3}}, ); ``` ### Grouping With `groupBy`, you get one row per group containing the group fields plus the computed values. Without it, a single summary row is returned. ```dart // One row per status: [{'status': 'open', 'count': 12, 'avg': 2.1}, ...] final byStatus = await db.aggregate('todos', aggregates: {'count': '*', 'avg': 'priority'}, groupBy: ['status'], ); for (final row in byStatus) { print('${row['status']}: ${row['count']} (avg ${row['avg']})'); } ``` ## Raw query form `db.count` and `db.aggregate` are convenience wrappers over the `$aggregate` query form. You can also use it directly with `queryOnce`: ```dart final result = await db.queryOnce({ 'todos': { 'where': {'done': false}, r'$aggregate': {'count': '*'}, r'$groupBy': ['status'], }, }); ``` ## Next Steps - [Basic Queries](/docs/queries/basics) - Filtering, sorting, and reactive queries - [Operators](/docs/queries/operators) - Comparison and logical operators for `where` - [Pagination & Fields](/docs/queries/pagination) - Cursor pagination and projection --- # Basic Queries > Learn how to query data with Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/queries/basics InstantDB uses InstaQL, a simple yet powerful query language designed for real-time applications. All queries are reactive by default, meaning your UI automatically updates when data changes. ## Simple Queries ### Fetch All Records Query all records of a specific entity type: ```dart // Get all todos final todosQuery = db.query({'todos': {}}); // Access the data Watch((context) { final result = todosQuery.value; if (result.isLoading) { return const CircularProgressIndicator(); } if (result.hasError) { return Text('Error: ${result.error}'); } final todos = result.data!['todos'] as List; return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile(todo: todos[index]), ); }); ``` ### Filter with Where Clauses Filter data using where conditions: ```dart // Get completed todos only final completedTodos = db.query({ 'todos': { 'where': {'completed': true}, }, }); // Get todos created today final today = DateTime.now(); final startOfDay = DateTime(today.year, today.month, today.day); final todaysTodos = db.query({ 'todos': { 'where': { 'createdAt': {'\$gte': startOfDay.millisecondsSinceEpoch}, }, }, }); ``` ### Sorting and Limiting Control the order and number of results: ```dart // Get latest 10 todos, sorted by creation date final latestTodos = db.query({ 'todos': { '\$': { 'order': {'createdAt': 'desc'}, 'limit': 10, }, }, }); // Multiple sort fields final sortedTodos = db.query({ 'todos': { '\$': { 'order': [ {'priority': 'desc'}, {'createdAt': 'asc'}, ], }, }, }); ``` ## Query Operators ### Comparison Operators | Operator | Description | Example | |----------|-------------|---------| | `\$eq` | Equal (default) | `{'status': 'active'}` | | `\$neq` | Not equal | `{'status': {'\$neq': 'deleted'}}` | | `\$gt` | Greater than | `{'score': {'\$gt': 100}}` | | `\$gte` | Greater than or equal | `{'age': {'\$gte': 18}}` | | `\$lt` | Less than | `{'price': {'\$lt': 50}}` | | `\$lte` | Less than or equal | `{'quantity': {'\$lte': 10}}` | ### Array Operators | Operator | Description | Example | |----------|-------------|---------| | `\$in` | Value in array | `{'category': {'\$in': ['work', 'personal']}}` | | `\$nin` | Value not in array | `{'status': {'\$nin': ['deleted', 'archived']}}` | ### String Operators | Operator | Description | Example | |----------|-------------|---------| | `\$contains` | Contains substring | `{'title': {'\$contains': 'urgent'}}` | | `\$startsWith` | Starts with | `{'email': {'\$startsWith': 'admin'}}` | | `\$endsWith` | Ends with | `{'filename': {'\$endsWith': '.pdf'}}` | ## React-Style Query Syntax Flutter InstantDB supports React-style query syntax for compatibility: ```dart final query = db.query({ 'todos': { '\$': { 'where': {'completed': false}, 'order': {'createdAt': 'desc'}, 'limit': 20, }, }, }); ``` ```dart final query = db.query({ 'todos': { 'where': {'completed': false}, 'orderBy': [{'createdAt': 'desc'}], 'limit': 20, }, }); ``` Both syntaxes are supported and can be used interchangeably. ## Reactive Queries ### Using InstantBuilder The recommended way to use queries in widgets: ```dart InstantBuilder( query: { 'todos': { 'where': {'userId': currentUserId}, }, }, builder: (context, result) { if (result.isLoading) { return const CircularProgressIndicator(); } if (result.hasError) { return Text('Error: ${result.error}'); } final todos = result.data!['todos'] as List; return TodosList(todos: todos); }, ) ``` ### Using InstantBuilderTyped For better type safety, use the typed version: ```dart InstantBuilderTyped>>( query: {'todos': {}}, transformer: (data) { final todos = (data['todos'] as List).cast>(); // Apply client-side sorting or filtering if needed todos.sort((a, b) => b['createdAt'].compareTo(a['createdAt'])); return todos; }, builder: (context, todos) { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile(todo: todos[index]), ); }, ) ``` ## One-time Queries For non-reactive queries that execute once: ```dart // Execute query once and get result final result = await db.queryOnce({'todos': {}}); if (result.hasData) { final todos = result.data!['todos'] as List; print('Found ${todos.length} todos'); } ``` ## Complex Queries ### Multiple Conditions Combine multiple where conditions: ```dart final complexQuery = db.query({ 'todos': { 'where': { 'completed': false, 'priority': {'\$in': ['high', 'urgent']}, 'dueDate': {'\$lte': DateTime.now().millisecondsSinceEpoch}, 'assignee': {'\$neq': null}, }, }, }); ``` ### Nested Conditions Use logical operators for complex conditions: ```dart final nestedQuery = db.query({ 'todos': { 'where': { '\$or': [ {'priority': 'urgent'}, { '\$and': [ {'priority': 'high'}, {'dueDate': {'\$lte': DateTime.now().millisecondsSinceEpoch}}, ], }, ], }, }, }); ``` ## Query Performance ### Indexing Ensure your frequently queried fields are indexed in your InstantDB schema: ```dart // This will perform better if 'userId' is indexed final userTodos = db.query({ 'todos': { 'where': {'userId': currentUserId}, }, }); ``` ### Pagination Use limit and offset for large datasets: ```dart final page1 = db.query({ 'todos': { '\$': { 'limit': 20, 'offset': 0, }, }, }); final page2 = db.query({ 'todos': { '\$': { 'limit': 20, 'offset': 20, }, }, }); ``` ## Error Handling Handle query errors gracefully: ```dart InstantBuilder( query: {'todos': {}}, errorBuilder: (context, error) { return Column( children: [ const Icon(Icons.error, color: Colors.red), Text('Failed to load todos: $error'), ElevatedButton( onPressed: () { // Retry logic }, child: const Text('Retry'), ), ], ); }, builder: (context, result) { // Success case final todos = result.data!['todos'] as List; return TodosList(todos: todos); }, ) ``` ## Next Steps Learn more about advanced querying features: - [Operators](/docs/queries/operators) - String matching, negation, and logical combinators - [Pagination & Fields](/docs/queries/pagination) - Cursor pagination, projection, infinite queries - [Typed Query DSL](/docs/typed/query-dsl) - Compile-time-safe queries - [Real-time Updates](/docs/realtime/sync) - Understanding live data - [Schema Validation](/docs/concepts/schema) - Type-safe queries --- # Query Operators > String matching, negation, logical combinators, and dot-notation in where clauses Source: https://flutter-instantdb.vercel.app/docs/queries/operators InstantDB's `where` clauses support a set of operators for matching, negation, and logical composition, in addition to the comparison and array operators covered in [Basics](/docs/queries/basics). ## String matching `$like` (case-sensitive) and `$ilike` (case-insensitive) match against SQL-style patterns using `%` (any sequence) and `_` (single character) wildcards. ```dart // Case-sensitive: titles containing "urgent" final urgent = db.query({ 'todos': { 'where': { 'title': {'\$like': '%urgent%'}, }, }, }); // Case-insensitive: titles starting with "ship" final shipping = db.query({ 'todos': { 'where': { 'title': {'\$ilike': 'ship%'}, }, }, }); ``` ## Negation `$not` matches values that are not equal to the operand (an alias of `$ne`). ```dart final notDone = db.query({ 'todos': { 'where': { 'status': {'\$not': 'done'}, }, }, }); ``` ## Logical combinators Use `and` / `or` to combine multiple conditions. Each takes a list of sub-conditions. ```dart // AND: high priority AND not completed final q = db.query({ 'todos': { 'where': { 'and': [ {'priority': {'\$gte': 8}}, {'completed': false}, ], }, }, }); // OR: urgent OR overdue final q2 = db.query({ 'todos': { 'where': { 'or': [ {'priority': 'urgent'}, {'dueDate': {'\$lt': DateTime.now().millisecondsSinceEpoch}}, ], }, }, }); ``` ## Dot-notation nested fields Match on a nested or related field using a dotted key: ```dart final q = db.query({ 'goals': { 'where': { 'todos.title': 'Run', }, }, }); ``` ## Other supported operators `$eq` (default equality), `$nin` (not in array), and `$exists` remain supported: ```dart db.query({ 'todos': { 'where': { 'status': {'\$nin': ['deleted', 'archived']}, 'assignee': {'\$exists': true}, }, }, }); ``` ## Typed equivalents If you use the [typed query DSL](/docs/typed/query-dsl), these operators are exposed as type-checked methods: `like` / `ilike` on `Col`, comparisons on `Col`, and `&` / `|` for `and` / `or`. --- # Pagination & Fields > Cursor pagination, pageInfo, field projection, and infinite queries Source: https://flutter-instantdb.vercel.app/docs/queries/pagination Beyond `limit` / `offset` (covered in [Basics](/docs/queries/basics)), InstantDB supports cursor pagination, a `pageInfo` cursor summary, field projection, and an accumulating infinite query. ## Cursor pagination Under a namespace's `$` options, use `first` / `after` for forward paging and `last` / `before` for backward paging. `afterInclusive` / `beforeInclusive` control whether the cursor row itself is included. ```dart // First page: leading 2 by ascending order final firstPage = await db.queryOnce({ 'todos': { '\$': {'order': {'n': 'asc'}, 'first': 2}, }, }); ``` ## pageInfo When a query paginates, `QueryResult.pageInfo` carries a per-namespace cursor summary: `startCursor`, `endCursor`, `hasNextPage`, and `hasPreviousPage`. It is `null` when the query did not paginate. ```dart final r = await db.queryOnce({ 'todos': { '\$': {'order': {'n': 'asc'}, 'first': 2}, }, }); r.pageInfo?['todos']?['hasNextPage']; // true r.pageInfo?['todos']?['hasPreviousPage']; // false final cursor = r.pageInfo?['todos']?['endCursor'] as String; ``` Use `endCursor` as the `after` value to fetch the next page: ```dart final next = await db.queryOnce({ 'todos': { '\$': {'order': {'n': 'asc'}, 'first': 2, 'after': cursor}, }, }); ``` ## Field projection Use `fields` under `$` to fetch only specific attributes. The `id` is always included. ```dart final r = await db.queryOnce({ 'todos': { '\$': {'fields': ['title', 'status']}, }, }); ``` ## Infinite queries `db.infiniteQuery(...)` returns an accumulator that concatenates items across pages and advances via `loadMore()`. `pageSize` becomes the `first` count; `entityType` is the namespace to paginate. Pair it with the `InstantInfiniteBuilder` widget. ```dart final inf = db.infiniteQuery( { 'todos': { '\$': {'order': {'n': 'asc'}, 'first': 2}, }, }, pageSize: 2, entityType: 'todos', ); // inf.items -> Signal>> (accumulated) // inf.hasMore -> Signal // inf.isLoading -> Signal await inf.loadMore(); // append the next page // Dispose when done inf.dispose(); ``` ```dart InstantInfiniteBuilder( query: inf, builder: (context, items, hasMore) { return ListView.builder( itemCount: items.length + (hasMore ? 1 : 0), itemBuilder: (context, index) { if (index >= items.length) { inf.loadMore(); return const Center(child: CircularProgressIndicator()); } return TodoTile(todo: items[index]); }, ); }, ) ``` ## Per-relation pageInfo Cursor-paginated nested `include` relations surface their own `pageInfo` under a composite key. See [Typed Relations](/docs/typed/relations#per-relation-pageinfo) for details — the key looks like `result.pageInfo?['goals.todos']`. ## Typed equivalent The [typed query DSL](/docs/typed/query-dsl#pagination-and-limits) exposes `first` / `last` / `after` / `before` / `afterInclusive` / `beforeInclusive` and `select` as type-checked builder methods, returning the same `QueryResult` with the same `pageInfo`. --- # Collaborative Features > Build multi-user collaborative experiences with Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/realtime/collaboration Build powerful collaborative applications with InstantDB's real-time synchronization, presence system, and conflict resolution. Create experiences like shared whiteboards, collaborative editors, multiplayer games, and team workspaces. ## Complete Collaborative Editor Here's a full example of a collaborative text editor with cursors, typing indicators, and presence: ```dart class CollaborativeEditor extends StatefulWidget { @override State createState() => _CollaborativeEditorState(); } class _CollaborativeEditorState extends State { final TextEditingController _controller = TextEditingController(); final FocusNode _focusNode = FocusNode(); InstantRoom? _room; String? _documentId; Timer? _typingTimer; @override void initState() { super.initState(); _documentId = 'doc-${widget.documentId}'; _initializeCollaboration(); } void _initializeCollaboration() { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; // Join collaboration room _room = db.presence.joinRoom(_documentId!, initialPresence: { 'userName': currentUser?.email ?? 'Anonymous', 'status': 'editing', 'color': _generateUserColor(currentUser?.id ?? 'anonymous'), }); // Listen to document changes _subscribeToDocument(); // Handle text selection changes for cursor position _controller.addListener(_updateCursorPosition); } void _subscribeToDocument() { final db = InstantProvider.of(context); // Subscribe to document content changes db.subscribeQuery({ 'documents': { 'where': {'id': _documentId}, } }).stream.listen((result) { final documents = result.data?['documents'] as List? ?? []; if (documents.isNotEmpty) { final document = documents.first as Map; final content = document['content'] as String? ?? ''; // Update content if different (avoid cursor jumps) if (_controller.text != content) { final selection = _controller.selection; _controller.text = content; _controller.selection = selection; } } }); } void _updateCursorPosition() { final selection = _controller.selection; if (selection.isValid && _room != null) { _room!.updateCursor( x: selection.baseOffset.toDouble(), y: 0, // For text, we use line-based positioning ); } } void _handleTextChange(String text) { // Update document with debouncing _typingTimer?.cancel(); _room?.setTyping(true); _typingTimer = Timer(const Duration(milliseconds: 500), () async { await _saveDocument(text); _room?.setTyping(false); }); } Future _saveDocument(String content) async { final db = InstantProvider.of(context); await db.transact([ db.update(_documentId!, { 'content': content, 'lastModified': DateTime.now().millisecondsSinceEpoch, }), ]); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Collaborative Editor'), actions: [ // Show connected users UserAvatars(room: _room!), const SizedBox(width: 16), ], ), body: Column( children: [ // Connection status ConnectionStatusBanner(), // Typing indicators TypingIndicatorBanner(room: _room!), // Editor with cursor overlay Expanded( child: Stack( children: [ // Main text editor Padding( padding: const EdgeInsets.all(16), child: TextField( controller: _controller, focusNode: _focusNode, maxLines: null, expands: true, onChanged: _handleTextChange, decoration: const InputDecoration( border: InputBorder.none, hintText: 'Start typing...', ), ), ), // Collaborative cursors overlay CursorOverlay(room: _room!), ], ), ), ], ), ); } } ``` ## Real-time Whiteboard Create a collaborative drawing canvas: ```dart class CollaborativeWhiteboard extends StatefulWidget { @override State createState() => _CollaborativeWhiteboardState(); } class _CollaborativeWhiteboardState extends State { InstantRoom? _room; final List _points = []; String? _currentStroke; @override void initState() { super.initState(); _initializeWhiteboard(); } void _initializeWhiteboard() { final db = InstantProvider.of(context); _room = db.presence.joinRoom('whiteboard', initialPresence: { 'userName': 'Artist ${DateTime.now().millisecondsSinceEpoch % 1000}', 'tool': 'pen', 'color': '#000000', }); // Subscribe to drawing strokes db.subscribeQuery({ 'strokes': { 'where': {'whiteboardId': 'main'}, 'orderBy': {'createdAt': 'asc'}, } }).stream.listen((result) { final strokes = result.data?['strokes'] as List? ?? []; setState(() { _points.clear(); _points.addAll(strokes.map((s) => DrawingPoint.fromJson(s))); }); }); } void _handlePanStart(DragStartDetails details) { _currentStroke = db.id(); _room?.setPresence({'status': 'drawing'}); final point = DrawingPoint( id: _currentStroke!, x: details.localPosition.dx, y: details.localPosition.dy, isStart: true, ); _addPoint(point); } void _handlePanUpdate(DragUpdateDetails details) { if (_currentStroke == null) return; final point = DrawingPoint( id: _currentStroke!, x: details.localPosition.dx, y: details.localPosition.dy, isStart: false, ); _addPoint(point); // Update cursor position for others _room?.updateCursor( x: details.localPosition.dx, y: details.localPosition.dy, ); } void _handlePanEnd(DragEndDetails details) { _currentStroke = null; _room?.setPresence({'status': 'idle'}); } Future _addPoint(DrawingPoint point) async { final db = InstantProvider.of(context); await db.transact([ ...db.create('points', { 'id': db.id(), 'strokeId': point.id, 'x': point.x, 'y': point.y, 'isStart': point.isStart, 'whiteboardId': 'main', 'createdAt': DateTime.now().millisecondsSinceEpoch, }), ]); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Collaborative Whiteboard'), actions: [ UserAvatars(room: _room!), ], ), body: Stack( children: [ // Drawing canvas GestureDetector( onPanStart: _handlePanStart, onPanUpdate: _handlePanUpdate, onPanEnd: _handlePanEnd, child: CustomPaint( painter: WhiteboardPainter(_points), size: Size.infinite, ), ), // Collaborative cursors CursorOverlay(room: _room!), // Reactions overlay ReactionsOverlay(room: _room!), ], ), floatingActionButton: FloatingActionButton( onPressed: _clearCanvas, child: const Icon(Icons.clear), ), ); } } ``` ## Team Chat with Presence Build a team chat with rich presence information: ```dart class TeamChat extends StatefulWidget { final String teamId; const TeamChat({super.key, required this.teamId}); @override State createState() => _TeamChatState(); } class _TeamChatState extends State { final TextEditingController _messageController = TextEditingController(); InstantRoom? _room; StreamSubscription? _chatSubscription; final List _messages = []; @override void initState() { super.initState(); _initializeChat(); } void _initializeChat() { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; // Join team room _room = db.presence.joinRoom('team-${widget.teamId}', initialPresence: { 'userName': currentUser?.email ?? 'Anonymous', 'status': 'online', 'avatar': currentUser?.metadata?['avatar'], 'lastSeen': DateTime.now().millisecondsSinceEpoch, }); // Subscribe to chat messages _chatSubscription = _room!.subscribeTopic('messages').listen((data) { final message = ChatMessage.fromJson(data); setState(() { _messages.add(message); _messages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); }); }); // Load existing messages _loadChatHistory(); // Update typing status _messageController.addListener(_handleTyping); } Timer? _typingTimer; void _handleTyping() { _room?.setTyping(true); _typingTimer?.cancel(); _typingTimer = Timer(const Duration(seconds: 2), () { _room?.setTyping(false); }); } Future _loadChatHistory() async { final db = InstantProvider.of(context); final result = await db.queryOnce({ 'messages': { 'where': {'teamId': widget.teamId}, 'orderBy': {'timestamp': 'asc'}, 'limit': 100, } }); final messages = result.data?['messages'] as List? ?? []; setState(() { _messages.clear(); _messages.addAll(messages.map((m) => ChatMessage.fromJson(m))); }); } Future _sendMessage() async { final text = _messageController.text.trim(); if (text.isEmpty) return; final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; final message = { 'id': db.id(), 'teamId': widget.teamId, 'userId': currentUser?.id ?? 'anonymous', 'userName': currentUser?.email ?? 'Anonymous', 'text': text, 'timestamp': DateTime.now().millisecondsSinceEpoch, }; // Save to database await db.transact([ ...db.create('messages', message), ]); // Broadcast via presence await _room!.publishTopic('messages', message); _messageController.clear(); _room?.setTyping(false); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Team ${widget.teamId}'), actions: [ // Team presence indicators Watch((context) { final presence = _room?.getPresence().value ?? {}; final onlineCount = presence.values .where((p) => p.data['status'] == 'online') .length; return Padding( padding: const EdgeInsets.all(8.0), child: Chip( label: Text('$onlineCount online'), avatar: const Icon(Icons.people, size: 16), ), ); }), ], ), body: Column( children: [ // Messages list Expanded( child: ListView.builder( itemCount: _messages.length, itemBuilder: (context, index) { final message = _messages[index]; return MessageBubble( message: message, isOwn: message.userId == InstantProvider.of(context).auth.currentUser.value?.id, ); }, ), ), // Typing indicators TypingIndicator(room: _room!), // Message input Container( padding: const EdgeInsets.all(8), child: Row( children: [ Expanded( child: TextField( controller: _messageController, decoration: const InputDecoration( hintText: 'Type a message...', border: OutlineInputBorder(), ), onSubmitted: (_) => _sendMessage(), ), ), const SizedBox(width: 8), IconButton( onPressed: _sendMessage, icon: const Icon(Icons.send), ), ], ), ), ], ), ); } } ``` ## Collaborative Task Management Build a shared task board with real-time updates: ```dart class CollaborativeTaskBoard extends StatefulWidget { @override State createState() => _CollaborativeTaskBoardState(); } class _CollaborativeTaskBoardState extends State { InstantRoom? _room; @override void initState() { super.initState(); _initializeTaskBoard(); } void _initializeTaskBoard() { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; _room = db.presence.joinRoom('task-board', initialPresence: { 'userName': currentUser?.email ?? 'Team Member', 'status': 'viewing', 'currentColumn': null, }); } Future _moveTask(String taskId, String toColumn) async { final db = InstantProvider.of(context); // Optimistic update with presence _room?.setPresence({ 'status': 'moving_task', 'taskId': taskId, 'toColumn': toColumn, }); // Send reaction for visual feedback await _room?.sendReaction('📋', metadata: { 'action': 'task_moved', 'taskId': taskId, 'column': toColumn, }); await db.transact([ db.update(taskId, { 'status': toColumn, 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]); _room?.setPresence({'status': 'viewing'}); } @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Scaffold( appBar: AppBar( title: const Text('Task Board'), actions: [ UserAvatars(room: _room!), IconButton( onPressed: _addNewTask, icon: const Icon(Icons.add), ), ], ), body: Row( children: [ // Task columns Expanded( child: InstantBuilder( query: { 'tasks': { 'where': {'boardId': 'main'}, 'orderBy': {'createdAt': 'desc'}, } }, builder: (context, result) { final tasks = result.data?['tasks'] as List? ?? []; return Row( children: [ TaskColumn( title: 'To Do', status: 'todo', tasks: tasks.where((t) => t['status'] == 'todo').toList(), onTaskMoved: _moveTask, room: _room!, ), TaskColumn( title: 'In Progress', status: 'inprogress', tasks: tasks.where((t) => t['status'] == 'inprogress').toList(), onTaskMoved: _moveTask, room: _room!, ), TaskColumn( title: 'Done', status: 'done', tasks: tasks.where((t) => t['status'] == 'done').toList(), onTaskMoved: _moveTask, room: _room!, ), ], ); }, ), ), ], ), ); } } ``` ## Conflict Resolution InstantDB automatically handles conflicts, but you can implement custom resolution: ```dart class ConflictAwareDocument extends StatefulWidget { @override State createState() => _ConflictAwareDocumentState(); } class _ConflictAwareDocumentState extends State { final TextEditingController _controller = TextEditingController(); String? _localVersion; String? _serverVersion; void _handleConflict(String localContent, String serverContent) { if (localContent == serverContent) return; // Show conflict resolution UI showDialog( context: context, builder: (context) => ConflictResolutionDialog( localVersion: localContent, serverVersion: serverContent, onResolved: _resolveConflict, ), ); } Future _resolveConflict(String resolvedContent) async { final db = InstantProvider.of(context); await db.transact([ db.update('document-id', { 'content': resolvedContent, 'lastModified': DateTime.now().millisecondsSinceEpoch, 'resolvedBy': db.auth.currentUser.value?.id, }), ]); _controller.text = resolvedContent; } @override Widget build(BuildContext context) { return InstantBuilder( query: {'documents': {'where': {'id': 'document-id'}}}, builder: (context, result) { final documents = result.data?['documents'] as List? ?? []; if (documents.isNotEmpty) { final document = documents.first as Map; final serverContent = document['content'] as String? ?? ''; // Check for conflicts if (_localVersion != null && _localVersion != serverContent && _controller.text != serverContent) { WidgetsBinding.instance.addPostFrameCallback((_) { _handleConflict(_controller.text, serverContent); }); } } return TextField( controller: _controller, onChanged: (text) => _localVersion = text, ); }, ); } } ``` ## Performance Optimization Optimize collaborative features for large teams: ```dart class OptimizedCollaboration { static const int MAX_CURSORS = 10; static const Duration PRESENCE_THROTTLE = Duration(milliseconds: 100); static const Duration TYPING_DEBOUNCE = Duration(milliseconds: 300); // Throttle cursor updates static Timer? _cursorTimer; static void updateCursor(InstantRoom room, double x, double y) { _cursorTimer?.cancel(); _cursorTimer = Timer(PRESENCE_THROTTLE, () { room.updateCursor(x: x, y: y); }); } // Limit displayed cursors static List> getLimitedCursors( Map allCursors, ) { final entries = allCursors.entries.toList(); entries.sort((a, b) { final aTime = a.value.data['lastUpdate'] ?? 0; final bTime = b.value.data['lastUpdate'] ?? 0; return bTime.compareTo(aTime); }); return entries.take(MAX_CURSORS).toList(); } // Batch presence updates static void batchPresenceUpdate( InstantRoom room, Map updates, ) { Timer(PRESENCE_THROTTLE, () { room.setPresence(updates); }); } } ``` ## Best Practices ### 1. Handle Connection States Always show connection status in collaborative apps: ```dart Widget buildConnectionAwareUI() { return Column( children: [ ConnectionStatusBanner(), // Your collaborative UI ], ); } ``` ### 2. Provide Visual Feedback Show user actions with reactions and animations: ```dart void _showCollaborativeAction(String action, String user) { _room?.sendReaction('✨', metadata: { 'action': action, 'user': user, 'timestamp': DateTime.now().millisecondsSinceEpoch, }); } ``` ### 3. Handle Graceful Degradation Ensure your app works offline: ```dart Widget buildOfflineCapableFeature() { return Watch((context) { final isOnline = db.syncEngine?.connectionStatus.value ?? false; return Column( children: [ if (!isOnline) OfflineIndicator(), // Feature works regardless of connection YourFeature(), ], ); }); } ``` ### 4. Clean Up Resources Always clean up presence and subscriptions: ```dart @override void dispose() { _room?.setPresence({'status': 'offline'}); _chatSubscription?.cancel(); _typingTimer?.cancel(); super.dispose(); } ``` ## Next Steps Explore more collaborative patterns: - [Presence System](/docs/realtime/presence) - Detailed presence API reference - [WebSocket Sync](/docs/realtime/sync) - Understanding real-time synchronization - [Authentication](/docs/auth/users) - User management for collaboration - [Performance Tips](/docs/advanced/performance) - Optimizing collaborative features --- # Presence System > Real-time collaboration with cursors, typing indicators, and reactions Source: https://flutter-instantdb.vercel.app/docs/realtime/presence InstantDB's presence system enables real-time collaboration features like cursors, typing indicators, reactions, and user avatars. It's perfect for building collaborative applications. ## Room-Based Presence The modern approach uses room-based APIs for better organization and scoping: ### Joining a Room ```dart class CollaborativeEditor extends StatefulWidget { @override State createState() => _CollaborativeEditorState(); } class _CollaborativeEditorState extends State { String? _userId; String? _userName; InstantRoom? _room; @override void initState() { super.initState(); _initializePresence(); } void _initializePresence() { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; // Use authenticated user or generate anonymous identity if (currentUser != null) { _userId = currentUser.id; _userName = currentUser.email; } else { _userId = db.getAnonymousUserId(); _userName = 'Guest ${_userId!.substring(_userId!.length - 4)}'; } // Join room with initial presence data _room = db.presence.joinRoom('editor-room', initialPresence: { 'userName': _userName, 'status': 'editing', 'avatar': _generateAvatar(_userName!), }); } } ``` ### Basic Presence Operations ```dart // Update user presence await _room!.setPresence({ 'status': 'typing', 'lastSeen': DateTime.now().millisecondsSinceEpoch, }); // Update cursor position await _room!.updateCursor(x: 100, y: 200); // Set typing indicator await _room!.setTyping(true); // Send a reaction await _room!.sendReaction('👍', metadata: { 'x': mouseX, 'y': mouseY, 'message': 'Great idea!', }); ``` ## Displaying Presence Data ### User Avatars Show all connected users in a room: ```dart class UserAvatars extends StatelessWidget { final InstantRoom room; const UserAvatars({super.key, required this.room}); @override Widget build(BuildContext context) { return Watch((context) { final presence = room.getPresence().value; final users = presence.entries .where((entry) => entry.value.data['status'] == 'online') .take(5) .toList(); return Row( children: [ ...users.map((entry) { final user = entry.value.data; return Padding( padding: const EdgeInsets.only(right: 8), child: CircleAvatar( radius: 16, backgroundColor: _getUserColor(user['userName']), child: Text( _getInitials(user['userName'] ?? '?'), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ); }), if (presence.length > 5) CircleAvatar( radius: 16, backgroundColor: Colors.grey, child: Text( '+${presence.length - 5}', style: const TextStyle(fontSize: 10, color: Colors.white), ), ), ], ); }); } } ``` ### Live Cursors Display real-time cursor positions: ```dart class CursorOverlay extends StatelessWidget { final InstantRoom room; final Widget child; const CursorOverlay({super.key, required this.room, required this.child}); @override Widget build(BuildContext context) { return Stack( children: [ child, // Cursor layer Watch((context) { final cursors = room.getCursors().value; return Stack( children: cursors.entries.map((entry) { final cursor = entry.value; final userName = cursor.data['userName'] ?? 'Unknown'; return Positioned( left: cursor.x, top: cursor.y, child: _CursorWidget( userName: userName, color: _getUserColor(userName), ), ); }).toList(), ); }), ], ); } } class _CursorWidget extends StatelessWidget { final String userName; final Color color; const _CursorWidget({required this.userName, required this.color}); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Cursor pointer CustomPaint( size: const Size(20, 20), painter: CursorPainter(color: color), ), // User name label Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(4), ), child: Text( userName, style: const TextStyle( fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500, ), ), ), ], ); } } ``` ### Typing Indicators Show who's currently typing: ```dart class TypingIndicator extends StatelessWidget { final InstantRoom room; const TypingIndicator({super.key, required this.room}); @override Widget build(BuildContext context) { return Watch((context) { final typing = room.getTyping().value; if (typing.isEmpty) { return const SizedBox.shrink(); } final typingUsers = typing.entries .map((entry) => entry.value.data['userName'] as String? ?? 'Someone') .toList(); String text; if (typingUsers.length == 1) { text = '${typingUsers.first} is typing...'; } else if (typingUsers.length == 2) { text = '${typingUsers.first} and ${typingUsers.last} are typing...'; } else { text = '${typingUsers.length} people are typing...'; } return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.grey.shade600), ), ), const SizedBox(width: 8), Text( text, style: TextStyle( fontSize: 12, color: Colors.grey.shade600, fontStyle: FontStyle.italic, ), ), ], ), ); }); } } ``` ### Reactions Display floating reactions: ```dart class ReactionsOverlay extends StatelessWidget { final InstantRoom room; final Widget child; const ReactionsOverlay({super.key, required this.room, required this.child}); @override Widget build(BuildContext context) { return Stack( children: [ child, // Reactions layer Watch((context) { final reactions = room.getReactions().value; return Stack( children: reactions.map((reaction) { final metadata = reaction.data['metadata'] as Map?; final x = metadata?['x']?.toDouble() ?? 0.0; final y = metadata?['y']?.toDouble() ?? 0.0; return Positioned( left: x, top: y, child: AnimatedReaction( emoji: reaction.data['reaction'] as String? ?? '❤️', onComplete: () { // Reaction animation completed }, ), ); }).toList(), ); }), ], ); } } class AnimatedReaction extends StatefulWidget { final String emoji; final VoidCallback onComplete; const AnimatedReaction({ super.key, required this.emoji, required this.onComplete, }); @override State createState() => _AnimatedReactionState(); } class _AnimatedReactionState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; late Animation _opacityAnimation; late Animation _positionAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, ); _scaleAnimation = Tween(begin: 0.5, end: 1.2).animate( CurvedAnimation(parent: _controller, curve: Curves.elasticOut), ); _opacityAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.7, 1.0, curve: Curves.easeOut), ), ); _positionAnimation = Tween( begin: Offset.zero, end: const Offset(0, -50), ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); _controller.forward().then((_) => widget.onComplete()); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.translate( offset: _positionAnimation.value, child: Transform.scale( scale: _scaleAnimation.value, child: Opacity( opacity: _opacityAnimation.value, child: Text( widget.emoji, style: const TextStyle(fontSize: 24), ), ), ), ); }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } } ``` ## Topic-Based Messaging Use topics for structured communication within rooms: ```dart class ChatRoom extends StatefulWidget { @override State createState() => _ChatRoomState(); } class _ChatRoomState extends State { InstantRoom? _room; final List _messages = []; StreamSubscription? _chatSubscription; @override void initState() { super.initState(); _initializeRoom(); } void _initializeRoom() { final db = InstantProvider.of(context); _room = db.presence.joinRoom('chat-room', initialPresence: { 'userName': 'Current User', 'status': 'online', }); // Subscribe to chat messages _chatSubscription = _room!.subscribeTopic('chat').listen((data) { final message = ChatMessage.fromJson(data); setState(() { _messages.add(message); }); }); } void _sendMessage(String text) { if (text.trim().isEmpty) return; final message = { 'id': DateTime.now().millisecondsSinceEpoch.toString(), 'text': text.trim(), 'userName': 'Current User', 'timestamp': DateTime.now().millisecondsSinceEpoch, }; _room!.publishTopic('chat', message); } } ``` ## Advanced Presence Features ### Presence with Custom Data Store custom data in presence: ```dart // Rich presence data await _room!.setPresence({ 'userName': 'Alice', 'status': 'editing', 'currentDocument': documentId, 'tool': 'text', 'mood': '😊', 'location': { 'section': 'introduction', 'paragraph': 3, }, }); ``` ### Temporary Presence Events Send temporary events that don't persist: ```dart // Temporary events (reactions, notifications) await _room!.sendReaction('🎉', metadata: { 'achievement': 'Document completed!', 'x': 200, 'y': 100, }); // These disappear after a short time ``` ### Presence Cleanup Always clean up presence when leaving: ```dart @override void dispose() { _room?.setPresence({'status': 'offline'}); _room = null; _chatSubscription?.cancel(); super.dispose(); } ``` ## Best Practices ### 1. Initialize Presence Early Set up presence as soon as users enter collaborative spaces: ```dart @override void initState() { super.initState(); // Initialize presence immediately _initializePresence(); } ``` ### 2. Throttle Updates Avoid excessive presence updates: ```dart Timer? _cursorTimer; void _updateCursor(double x, double y) { _cursorTimer?.cancel(); _cursorTimer = Timer(const Duration(milliseconds: 100), () { _room?.updateCursor(x: x, y: y); }); } ``` ### 3. Handle Anonymous Users Provide good defaults for anonymous users: ```dart String _generateGuestName(String userId) { return 'Guest ${userId.substring(userId.length - 4)}'; } ``` ### 4. Cleanup Resources Always clean up when leaving: ```dart void _leaveRoom() { _room?.setPresence({'status': 'offline'}); db.presence.leaveRoom('room-id'); } ``` ## Presence UI Patterns ### Status Indicators ```dart Widget buildStatusIndicator(String status) { Color color; IconData icon; switch (status) { case 'online': color = Colors.green; icon = Icons.circle; break; case 'typing': color = Colors.blue; icon = Icons.edit; break; case 'away': color = Colors.orange; icon = Icons.schedule; break; default: color = Colors.grey; icon = Icons.circle_outlined; } return Icon(icon, color: color, size: 12); } ``` ### User Count Badge ```dart Widget buildUserCount(int count) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(12), ), child: Text( '$count online', style: const TextStyle(color: Colors.white, fontSize: 12), ), ); } ``` ## Reactive Presence Widgets These widgets are the Flutter equivalents of InstantDB's React presence hooks. Each resolves the client via `InstantProvider.of(context)`, joins the room for you, and rebuilds on change. ### PresenceBuilder Equivalent of React `usePresence`. Joins the room, publishes `initialPresence`, rebuilds whenever a peer's presence changes, and leaves on dispose. The `InstantRoom` handle is passed to the builder so children can call `room.setPresence(...)`. ```dart PresenceBuilder( roomId: 'doc-42', initialPresence: {'name': 'Alice'}, builder: (context, room, peers) => Text('${peers.length} online'), ) ``` ### TopicListener Equivalent of React `useTopicEffect`. A side-effect widget that subscribes to a topic and invokes `onEvent` for each message, rendering `child` unchanged. ```dart TopicListener( roomId: 'doc-42', topic: 'emoji', onEvent: (data) => _showFloatingEmoji(data['emoji']), child: const Editor(), ) ``` ### TypingIndicatorBuilder Rebuilds with the current set of typing peers keyed by id. ```dart TypingIndicatorBuilder( roomId: 'doc-42', builder: (context, typing) => typing.isEmpty ? const SizedBox() : Text('${typing.length} typing…'), ) ``` ### ReactionsBuilder Rebuilds with the live list of reactions broadcast in the room. ```dart ReactionsBuilder( roomId: 'doc-42', builder: (context, reactions) => Wrap( children: [for (final r in reactions) Text(r.emoji)], ), ) ``` ### CursorOverlay Multiplayer cursor layer — the Flutter equivalent of InstantDB's ``. Wrap any content; it tracks the local pointer via `MouseRegion`, publishes it, and paints peers' cursors on top. ```dart CursorOverlay( roomId: 'doc-42', userName: 'Alice', userColor: '#E91E63', child: const Canvas(), ) ``` Provide a `cursorBuilder` to customize how remote cursors render: ```dart CursorOverlay( roomId: 'doc-42', cursorBuilder: (context, cursor) => Icon(Icons.navigation, color: Colors.pink), child: const Canvas(), ) ``` ## Next Steps Learn more about building collaborative features: - [Collaborative Features](/docs/realtime/collaboration) - Complete collaboration patterns - [Real-time Sync](/docs/realtime/sync) - Understanding data synchronization - [Flutter Widgets](/docs/flutter/widgets) - Reactive UI patterns - [Advanced Topics](/docs/advanced/performance) - Optimizing presence performance --- # WebSocket Synchronization > Real-time data synchronization with Flutter InstantDB Source: https://flutter-instantdb.vercel.app/docs/realtime/sync Flutter InstantDB provides real-time synchronization across all connected clients using WebSocket connections. Changes are propagated instantly with conflict resolution and offline support. ## Enabling Sync Enable real-time synchronization during database initialization: ```dart final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, // Enable real-time sync verboseLogging: false, // Set to true for detailed sync logs ), ); ``` ## Connection Status Monitor the connection status to provide user feedback: ```dart // Using a reactive widget Watch((context) { final syncEngine = db.syncEngine; final isConnected = syncEngine?.connectionStatus.value ?? false; return Row( children: [ Icon( isConnected ? Icons.cloud_done : Icons.cloud_off, color: isConnected ? Colors.green : Colors.grey, ), Text(isConnected ? 'Connected' : 'Offline'), ], ); }); // Or create a custom connection status widget class ConnectionStatusIndicator extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Watch((context) { final isOnline = db.syncEngine?.connectionStatus.value ?? false; return AnimatedContainer( duration: const Duration(milliseconds: 300), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: isOnline ? Colors.green : Colors.orange, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isOnline ? Icons.wifi : Icons.wifi_off, size: 16, color: Colors.white, ), const SizedBox(width: 4), Text( isOnline ? 'Online' : 'Offline', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500, ), ), ], ), ); }); } } ``` ### Lifecycle status with `connectionStatus` For the full connection lifecycle (not just a boolean), read `db.connectionStatus` — a `ReadonlySignal`. The `ConnectionStatus` enum has five values: | Value | Meaning | |-------|---------| | `connecting` | Socket is connecting | | `opened` | Socket open but not yet authenticated (pre `init-ok`) | | `authenticated` | Socket open and authenticated — fully online | | `closed` | Socket is closed | | `errored` | Socket errored | "Online" corresponds to `ConnectionStatus.authenticated`. ```dart Watch((context) { final status = db.connectionStatus.value; final online = status == ConnectionStatus.authenticated; return Text(online ? 'Online' : status.name); }); ``` #### `ConnectionStateBuilder` widget `ConnectionStateBuilder` rebuilds with the full lifecycle status: ```dart ConnectionStateBuilder( builder: (context, status) { switch (status) { case ConnectionStatus.authenticated: return const Text('Online'); case ConnectionStatus.connecting: case ConnectionStatus.opened: return const Text('Connecting...'); case ConnectionStatus.closed: case ConnectionStatus.errored: return const Text('Offline'); } }, ) ``` ### Stable local id `db.getLocalId(name)` returns a stable id for `name` that is persisted and survives restarts (matching `useLocalId` in the React SDK): ```dart final sessionId = await db.getLocalId('session'); ``` **Deprecations:** `db.isOnline` is deprecated — use `connectionStatus` (online == `ConnectionStatus.authenticated`). `db.getAnonymousUserId()` is deprecated — use `getLocalId(name)`. Both still work for now. ## How Sync Works ### Differential Sync InstantDB uses differential synchronization to efficiently sync only the changes: 1. **Local Changes**: When you make local changes, they're applied immediately (optimistic updates) 2. **Sync Queue**: Changes are queued for synchronization with the server 3. **WebSocket Transmission**: Changes are sent via WebSocket in real-time 4. **Server Processing**: Server processes and broadcasts changes to other clients 5. **Conflict Resolution**: Automatic handling of concurrent modifications ### Transaction Integrity All operations maintain transaction integrity during sync: ```dart // This transaction will be synced atomically await db.transact([ ...db.create('posts', { 'id': db.id(), 'title': 'New Post', 'authorId': userId, }), db.update(userId, { 'postCount': {'$increment': 1}, }), ]); ``` ## Sync Events Monitor sync events for debugging or user feedback: ```dart // Listen to sync engine events (if available) db.syncEngine?.onConnectionChange.listen((isConnected) { print('Connection status changed: $isConnected'); if (isConnected) { // Connection restored - sync pending changes showSnackBar('Connection restored'); } else { // Connection lost - work offline showSnackBar('Working offline'); } }); ``` ## Handling Offline/Online Transitions InstantDB handles offline scenarios gracefully: ### Offline Behavior When offline, InstantDB: - Stores all changes locally in SQLite - Continues to serve queries from local data - Queues mutations for later synchronization - Provides immediate UI updates (optimistic) ### Coming Back Online When connection is restored: - Pending transactions are automatically synced - Server changes are downloaded and applied - Conflicts are resolved automatically - UI updates reflect the synchronized state ```dart // Example of handling online/offline states class OfflineAwareWidget extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Watch((context) { final isOnline = db.syncEngine?.connectionStatus.value ?? false; return Column( children: [ if (!isOnline) Container( width: double.infinity, padding: const EdgeInsets.all(8), color: Colors.orange, child: const Text( 'Working offline - changes will sync when connected', textAlign: TextAlign.center, style: TextStyle(color: Colors.white), ), ), // Your main content InstantBuilder( query: {'todos': {}}, builder: (context, result) { // Data is always available, even offline return TodoList(todos: result.data!['todos']); }, ), ], ); }); } } ``` ## Sync Performance ### Batching Operations For better performance, batch multiple operations: ```dart // Instead of multiple separate transactions await db.transact([...db.create('item1', data1)]); await db.transact([...db.create('item2', data2)]); await db.transact([...db.create('item3', data3)]); // Use a single batched transaction await db.transact([ ...db.create('item1', data1), ...db.create('item2', data2), ...db.create('item3', data3), ]); ``` ### Optimistic Updates InstantDB provides optimistic updates by default: ```dart // UI updates immediately, then syncs in background await db.transact([ db.update(todoId, {'completed': true}), ]); // The UI shows the change instantly, sync happens asynchronously ``` ## Advanced Sync Configuration ### Custom Sync Settings Configure sync behavior during initialization: ```dart final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, // Custom WebSocket URL (optional) websocketUrl: 'wss://custom.instantdb.com/ws', // Enable detailed logging for debugging verboseLogging: true, // Custom API endpoint baseUrl: 'https://custom.instantdb.com', ), ); ``` ### Sync Debugging Enable verbose logging to debug sync issues: ```dart final db = await InstantDB.init( appId: 'your-app-id', config: const InstantConfig( syncEnabled: true, verboseLogging: true, // Detailed sync logs ), ); ``` This will log: - WebSocket connection events - Transaction synchronization - Conflict resolution - Network errors and retries ## Best Practices ### 1. Handle Connection States Always provide feedback for offline states: ```dart // Good: Show connection status if (!isOnline) { return OfflineBanner(); } // Bad: No indication of offline state ``` ### 2. Optimistic UI Updates Trust InstantDB's optimistic updates: ```dart // Good: Update immediately, let InstantDB handle sync await db.transact([db.update(id, newData)]); // Bad: Wait for server confirmation ``` ### 3. Batch Operations Group related operations into single transactions: ```dart // Good: Atomic operation await db.transact([ ...db.create('post', postData), db.update(userId, {'postCount': {'$increment': 1}}), ]); // Bad: Separate operations that could fail independently ``` ### 4. Monitor Connection Provide connection status in your UI: ```dart // Always show connection status in critical apps AppBar( actions: [ ConnectionStatusIndicator(), ], ) ``` ## Troubleshooting Sync Issues ### Common Issues 1. **Not Syncing**: Check if `syncEnabled: true` in config 2. **Slow Sync**: Check network connection and server status 3. **Conflicts**: Review conflict resolution logs 4. **Memory Issues**: Ensure proper disposal of database instances ### Debug Logging Enable verbose logging to diagnose issues: ```dart // Add this to see detailed sync logs config: const InstantConfig( syncEnabled: true, verboseLogging: true, ), ``` ### Network Debugging Test sync with network conditions: ```dart // Simulate offline mode for testing db.syncEngine?.disconnect(); // Reconnect db.syncEngine?.connect(); ``` ## Next Steps Learn more about InstantDB's real-time features: - [Presence System](/docs/realtime/presence) - Real-time collaboration - [Collaborative Features](/docs/realtime/collaboration) - Building multi-user apps - [Performance Tips](/docs/advanced/performance) - Optimizing sync performance - [Troubleshooting](/docs/advanced/troubleshooting) - Debugging sync issues --- # Code Generation > Generate typed tables from annotated models with flutter_instantdb_generator Source: https://flutter-instantdb.vercel.app/docs/typed/codegen Instead of hand-writing `InstantTable` classes, you can annotate a plain model class and let the build-time generator (Phase 6b) emit a typed table, columns, a `fromRow` mapper, and `getAll` / `watchAll` query helpers. ## Annotations - **`@InstantModel(entityType)`** — marks a class as a model. `entityType` is the namespace the generated table queries (e.g. `'todos'`). - **`@InstantField(name)`** — overrides the stored attribute name for a field. Without it, the field name is used as the attribute name. - **`@InstantLink()`** — marks a relation field; see [Relations](/docs/typed/relations). ```dart import 'package:flutter_instantdb/flutter_instantdb.dart'; part 'sample.instant.dart'; @InstantModel('gadgets') class Gadget { final String id; final String label; const Gadget({required this.id, required this.label}); } ``` Flat models (primitive fields) are fully supported. Relation/nested fields use `@InstantLink` (see [Relations](/docs/typed/relations)); non-nullable relations are rejected by the generator with guidance. ## The generator package The generator ships as a **separate dev-only package**, `flutter_instantdb_generator`, built on `build_runner` / `source_gen`. Add it (plus `build_runner`) to your `dev_dependencies`: ```yaml dev_dependencies: build_runner: ^2.4.13 flutter_instantdb_generator: ^0.1.0 ``` It is configured with `auto_apply: dependents` and `build_to: source`, so it runs on any package that depends on it and writes `.instant.dart` files next to your sources. ## Running the generator Make sure your model file has a `part 'your_file.instant.dart';` directive, then run: ```bash dart run build_runner build ``` (Use `dart run build_runner watch` to regenerate on save.) ## Generated output For the model above, the generator emits a `${Model}Table` with a `Col` per field, a `fromRow`, a scalar-only `toMap`, a `tx(db)` convenience, plus a `${Model}QueryX` extension carrying `getAll` / `watchAll` and a `${Model}TxX` extension for whole-model writes: ```dart class GadgetTable extends InstantModelTable { GadgetTable() : super('gadgets'); final id = const Col('id'); final label = const Col('label'); @override Gadget fromRow(Map m) => Gadget( id: m['id'] as String, label: m['label'] as String, ); Map toMap(Gadget m) => { 'id': m.id, 'label': m.label, }; TypedTx tx(InstantDB db) => db.txFor(this); } extension GadgetQueryX on TypedQuery { Future> getAll(InstantDB db) async => (await db.queryOnceTyped(this)) .documents .map(GadgetTable().fromRow) .toList(); ReadonlySignal> watchAll(InstantDB db) { final src = db.queryTyped(this); return computed( () => src.value.documents.map(GadgetTable().fromRow).toList()); } } ``` ## Using the generated table The generated table behaves like any [typed table](/docs/typed/query-dsl), and `getAll` / `watchAll` return typed `List` instead of raw maps: ```dart // One-shot, typed list final gadgets = await GadgetTable().query().getAll(db); // gadgets is List // Reactive, typed list final signal = GadgetTable() .query() .where((t) => t.label.ilike('%pro%')) .watchAll(db); Watch((context) { final gadgets = signal.value; // List return Text('${gadgets.length} gadgets'); }); ``` For whole-model writes (`createModel` / `updateModel` / `mergeModel`) and the `table.tx(db)` sugar, see [Transactions](/docs/typed/transactions). --- # Typed Layer Overview > Compile-time-safe queries and writes over the same InstantDB engine Source: https://flutter-instantdb.vercel.app/docs/typed/overview The typed layer adds a thin, compile-time-safe API on top of the untyped query and transaction engine. Queries and writes are expressed against typed column handles, so wrong field names and wrong value types fail to compile instead of failing at runtime. Everything compiles down to the exact same InstaQL maps and operations the untyped API produces — there is no separate engine and no runtime overhead. ## What you get - **Typed query DSL** — `Col`, `Filter`, `Order`, `InstantTable`, and `TypedQuery` let you build `where`/`order`/pagination/projection clauses with full type checking. See [Query DSL](/docs/typed/query-dsl). - **Code generation** — annotate a plain model class with `@InstantModel` and let `flutter_instantdb_generator` emit the table, columns, `fromRow`, and `getAll`/`watchAll` helpers. See [Code Generation](/docs/typed/codegen). - **Typed relations** — `@InstantLink` fields become typed relation accessors and typed `.include(...)`, with recursively-typed `fromRow`. See [Relations](/docs/typed/relations). - **Typed transactions** — `db.txFor(table)` gives a fluent, type-checked write builder (`create`/`update`/`merge`/`delete`/`link`/`unlink`/`lookup`) plus whole-model writes (`createModel`/`updateModel`/`mergeModel`). See [Transactions](/docs/typed/transactions). ## When to use it Reach for the typed layer when you have a known schema and want the compiler to catch mistakes — typos in field names, comparing a `String` column to an `int`, calling a string operator on a numeric column. The untyped map API remains fully supported and is the right choice for dynamic queries, projected relations, or schema-less data. The two layers interoperate freely: `db.transact` accepts typed writes alongside `List` and `TransactionChunk`, and typed queries return the same `QueryResult` as untyped ones. ## A quick taste ```dart // Typed query final q = Todos() .query() .where((t) => t.priority.gte(8) & t.title.ilike('%urgent%')) .order((t) => t.createdAt.desc()) .first(20); final result = await db.queryOnceTyped(q); // Typed write await db.transact( db.txFor(Todos()).create(id: db.id()) ..set(Todos().title, 'Ship it') ..set(Todos().priority, 9), ); ``` ## Importing Everything is exported from the main barrel: ```dart import 'package:flutter_instantdb/flutter_instantdb.dart'; ``` This brings in `Col`, `Filter`, `Order`, `InstantTable`, `InstantModelTable`, `TypedQuery`, `TypedTx`, `RelationRef`, and the `@InstantModel` / `@InstantField` / `@InstantLink` annotations. The code generator lives in a separate dev-only package (`flutter_instantdb_generator`). --- # Typed Query DSL > Build compile-time-safe InstaQL queries with Col, Filter, Order and TypedQuery Source: https://flutter-instantdb.vercel.app/docs/typed/query-dsl The typed query DSL (Phase 6a) lets you build queries against typed column handles. Every clause is type-checked at compile time, and `TypedQuery.toQuery()` compiles to the same InstaQL map the untyped engine consumes. ## Defining a table A table is a class extending `InstantTable` that declares a `Col` per field. Pass the namespace (entity type) to the superclass constructor. ```dart class Todos extends InstantTable { Todos() : super('todos'); final title = Col('title'); final priority = Col('priority'); final createdAt = Col('createdAt'); } ``` You can write tables by hand like this, or generate them from an annotated model — see [Code Generation](/docs/typed/codegen). ## Columns and operators `Col` exposes operators whose value types are bound to the column's `T`: ```dart final title = Col('title'); final priority = Col('priority'); title.eq('Run'); // {'title': 'Run'} title.ne('Run'); // {'title': {'$ne': 'Run'}} title.isNull(true); // {'title': {'$isNull': true}} priority.inList([1, 2]); // {'priority': {'$in': [1, 2]}} ``` ### Comparisons (Comparable only) `gt` / `gte` / `lt` / `lte` are available only on `Col` where `T` is `Comparable`: ```dart priority.gt(5); // {'priority': {'$gt': 5}} priority.gte(5); // {'priority': {'$gte': 5}} priority.lt(5); // {'priority': {'$lt': 5}} priority.lte(5); // {'priority': {'$lte': 5}} ``` ### String match (Col<String> only) `like` (case-sensitive) and `ilike` (case-insensitive) are available only on `Col` and accept SQL `%` / `_` wildcards: ```dart title.like('%x%'); // {'title': {'$like': '%x%'}} title.ilike('%x%'); // {'title': {'$ilike': '%x%'}} ``` Calling `priority.like(...)` or `title.gt(...)` is a compile error. ## Combining filters Combine leaf filters with `&` (and) and `|` (or): ```dart // AND final f = priority.gte(8) & title.ilike('%x%'); // {'and': [{'priority': {'$gte': 8}}, {'title': {'$ilike': '%x%'}}]} // OR final g = title.eq('A') | title.eq('B'); // {'or': [{'title': 'A'}, {'title': 'B'}]} ``` ## Ordering `Col.asc()` / `Col.desc()` produce an `Order`: ```dart Col('createdAt').asc(); // {'createdAt': 'asc'} Col('createdAt').desc(); // {'createdAt': 'desc'} ``` ## Building a query Start a query with `table.query()`, then chain fluent, **immutable** methods — each returns a new `TypedQuery`; the source query is never mutated. ```dart final q = Todos() .query() .where((t) => t.priority.gte(8) & t.title.ilike('%x%')) .order((t) => t.createdAt.desc()) .first(20) .select((t) => [t.title, t.priority]); q.toQuery(); // { // 'todos': { // '$': { // 'where': {'and': [{'priority': {'$gte': 8}}, {'title': {'$ilike': '%x%'}}]}, // 'order': {'createdAt': 'desc'}, // 'first': 20, // 'fields': ['title', 'priority'], // }, // }, // } ``` ### Pagination and limits `first` / `last` / `after` / `before` (cursor pagination), `afterInclusive` / `beforeInclusive`, and `limit` / `offset` are all available: ```dart final q = Todos() .query() .order((t) => t.createdAt.asc()) .first(2) .after('cursor1') .afterInclusive(true); ``` ### Projection `select` restricts the returned attributes (`id` is always included): ```dart Todos().query().select((t) => [t.title, t.priority]); ``` ## Running typed queries `db.queryTyped` returns a reactive `Signal`; `db.queryOnceTyped` runs once. ```dart final signal = db.queryTyped(Todos().query().where((t) => t.priority.gte(8))); Watch((context) { final result = signal.value; if (result.isLoading) return const CircularProgressIndicator(); final todos = result.data!['todos'] as List; return Text('${todos.length} todos'); }); ``` ```dart final result = await db.queryOnceTyped( Todos().query().where((t) => t.title.ilike('%urgent%')), ); final todos = result.data!['todos'] as List; ``` Both return the same `QueryResult` as the untyped API, so `result.pageInfo` and `result.documents` behave identically. To map result documents into typed model objects, see [Code Generation](/docs/typed/codegen) (the generated `getAll`/`watchAll` helpers). --- # Typed Relations > Model relations with @InstantLink, typed includes, and recursively-typed fromRow Source: https://flutter-instantdb.vercel.app/docs/typed/relations Relations let a model reference other models. With `@InstantLink` the generator emits typed relation accessors, a recursively-typed `fromRow`, and a `RelationRef` handle for typed link/unlink writes. ## Declaring relations Mark a relation field with `@InstantLink`. **Cardinality is inferred from the field type**: `List` is to-many, a bare `T` is to-one. The target (`T`) must itself be an `@InstantModel`. ```dart @InstantModel('gadgets') class Gadget { final String id; final String label; const Gadget({required this.id, required this.label}); } @InstantModel('widgets') class Widget2 { final String id; final String name; final int weight; @InstantLink() final List gadgets; // to-many (List) const Widget2({ required this.id, required this.name, required this.weight, required this.gadgets, }); } ``` `@InstantLink({attr: '...'})` overrides the stored relation attribute (the include key), which defaults to the field name. ## What the generator emits For each `@InstantLink` field the generator emits: - A **typed relation accessor** getter returning a `TypedQuery` of the target table, tagged with the relation attribute: ```dart TypedQuery get gadgets => TypedQuery(GadgetTable(), relationAttr: 'gadgets'); ``` - A **`RelationRef` const** for typed link/unlink writes (see [Transactions](/docs/typed/transactions)): ```dart static const gadgetsRel = RelationRef('gadgets'); ``` - A **recursively-typed `fromRow`** arm mapping included relation maps to `List` (to-many) or `T?` (to-one). Un-included relations safely yield `[]` / `null` via a `whereType` guard: ```dart gadgets: (m['gadgets'] as List?) ?.whereType>() .map(GadgetTable().fromRow) .toList() ?? const [], ``` ## Without code generation The generator is a convenience, not a requirement. The whole typed layer — `Col`, `RelationRef`, `InstantTable`, `TypedQuery`, `TypedTx` — is plain Dart you can write by hand. A relation is just a `RelationRef('storedAttr')`; linking is just an `update(...).link({attr: id})` under the hood. Declare the tables yourself: extend `InstantTable`, expose columns as `static const Col` fields, and expose each relation as a `static const RelationRef`: ```dart class UserTable extends InstantTable { UserTable() : super('users'); static const id = Col('id'); static const name = Col('name'); } class TodoTable extends InstantTable { TodoTable() : super('todos'); static const id = Col('id'); static const text = Col('text'); static const done = Col('done'); // Relation handle — no generator needed. 'author' is the stored attribute. static const authorRel = RelationRef('author'); } ``` ### Linking and unlinking `TypedTx` gives you two link APIs — neither needs generated code: ```dart final todos = db.txFor(TodoTable()); // Typed: pass the RelationRef you declared above. `targetIds` is one id or a List. await db.transact(todos.linkRel(todoId, TodoTable.authorRel, userId)); await db.transact(todos.unlinkRel(todoId, TodoTable.authorRel, userId)); // Untyped: pass the relation attribute as a plain string. await db.transact(todos.link(todoId, 'author', userId)); await db.transact(todos.unlink(todoId, 'author', userId)); ``` Both compile to the same `update(todoId).link({'author': userId})` op, so the fully untyped form works too if you skip `TypedTx` entirely: ```dart await db.transact(db.update(todoId).link({'author': userId})); ``` ### Including a relation by hand Without a generated relation accessor, build the included sub-query with the `relationAttr:` constructor argument — that string becomes the include key: ```dart final q = TodoTable().query().include( (t) => TypedQuery(UserTable(), relationAttr: 'author'), ); final r = await db.queryOnceTyped(q); // r.documents[i]['author'] holds the linked user map. ``` You read results from the raw `r.documents` maps (the recursively-typed `fromRow` is the only piece you give up by not generating). Reach for the generator when you want `fromRow`, typed relation accessor getters, and `toMap` written for you. ## Typed includes Use `TypedQuery.include((t) => t.relation...)` to fetch related entities. The relation sub-query supports nested `where` / `order` / `limit` / `offset`, cursor pagination, and recursive includes. Includes are immutable — the source query is never mutated. ```dart // Fetch goals, each with its linked todos final q = GoalTable().query().include((g) => g.todos); // Narrow the included set final q2 = GoalTable() .query() .include((g) => g.todos.where((t) => t.n.gte(2))); // Order + cursor window the relation final q3 = GoalTable() .query() .include((g) => g.todos.order((t) => t.n.asc()).first(1)); final r = await db.queryOnceTyped(q3); final goal = GoalTable().fromRow(r.documents.firstWhere((d) => d['id'] == 'g1')); final todos = goal.todos; // List, in order, windowed ``` Querying a parent **without** an `include` leaves relation fields empty — `fromRow` yields `[]` / `null` rather than crashing. ## The `.select()` restriction `include(...)` **throws `ArgumentError`** if the relation sub-query carries `.select()` (a fields projection). The generated `fromRow` hard-casts every field, so a projected map would cause a `TypeError`. Use the untyped map API if you need a projected relation. ```dart // Throws ArgumentError: GoalTable().query().include((g) => g.todos.select((t) => [t.n])); ``` ## Per-relation pageInfo When a nested relation include is cursor-paginated, the engine surfaces its `pageInfo` under a composite key `'.'`. Deeper nesting produces dotted keys. ```dart final q = GoalTable() .query() .include((g) => g.todos.order((t) => t.n.asc()).first(1)); final r = await db.queryOnceTyped(q); r.pageInfo?['goals.todos']?['hasNextPage']; // true // Deeper: r.pageInfo?['goals.todos.tags'] ``` Non-paginated includes add no composite pageInfo key. Note that pageInfo is per relation **path**, not per parent entity: with multiple parents, the key reflects the last parent's window. --- # Typed Transactions > Compile-time-safe writes with db.txFor, fluent set, whole-model writes, and typed relations Source: https://flutter-instantdb.vercel.app/docs/typed/transactions Typed transactions (Phases 6c–6e) give a fluent, type-checked write builder over the same operation engine as the untyped API. `set(Col, T)` binds each value's type to its column, so wrong-typed writes (e.g. `set(t.priority, 'x')`) do not compile. All ops delegate to the existing untyped builder — nothing is reimplemented. ## Entry point `db.txFor(table)` returns a `TypedTx` for the table. The result is accepted directly by `db.transact` (it implements `ToTransaction`). ```dart final t = Todos(); await db.transact( db.txFor(t).create(id: 't1') ..set(t.title, 'Run') ..set(t.priority, 3), ); ``` Cascades (`..set(..)..set(..)`) are the idiomatic way to fill fields. ## Operations ### create / update / merge ```dart // Create (id optional — generated if omitted) db.txFor(t).create(id: 't1')..set(t.title, 'Run')..set(t.priority, 1); // Update an existing entity db.txFor(t).update('t1')..set(t.priority, 2); // Deep-merge into an existing entity db.txFor(t).merge('t1')..set(t.priority, 5); ``` ### delete `delete` returns a `TransactionChunk` directly (no `set`): ```dart await db.transact(db.txFor(t).delete('t1')); ``` ### Strict mode (TxOpts) `opts(TxOpts(upsert: false))` controls upsert/strict behavior on `update` / `merge` (ignored for `create`). With `upsert: false`, the write does not create the entity if it does not exist. ```dart db.txFor(t).update('t1') ..set(t.priority, 9) ..opts(const TxOpts(upsert: false)); ``` ### Typed lookup (upsert by unique attribute) `lookup(Col, value)` targets an entity by a unique attribute instead of by id — an upsert. Pass `merge: true` to deep-merge instead of update. ```dart // Update-or-create the todo whose email == 'a@b.com' await db.transact( db.txFor(t).lookup(t.email, 'a@b.com')..set(t.title, 'First'), ); // Merge variant db.txFor(t).lookup(t.email, 'a@b.com', merge: true)..set(t.title, 'X'); ``` ### link / unlink Untyped relation link/unlink by attribute name (`targetId` is a single id or a `List`): ```dart db.txFor(t).link('t1', 'tags', 'g1'); db.txFor(t).unlink('t1', 'tags', 'g1'); ``` For type-checked relations, prefer `linkRel` / `unlinkRel` (below). ## Whole-model writes When you use the generator, each table gets a scalar-only `toMap`, and a `${Model}TxX` extension adds whole-model write helpers: - `createModel(Model)` — writes all scalar fields in one call. `data['id']` from the model is used as the entity id. - `updateModel(id, Model)` — writes all scalar fields of an existing entity. - `mergeModel(id, Model)` — deep-merges all scalar fields. ```dart await db.transact(db.txFor(GadgetTable()).createModel( const Gadget(id: 'g1', label: 'Spanner'), )); await db.transact(db.txFor(GadgetTable()).updateModel( 'g1', const Gadget(id: 'g1', label: 'Wrench'), )); await db.transact(db.txFor(GadgetTable()).mergeModel( 'g1', const Gadget(id: 'g1', label: 'Hammer'), )); ``` **`toMap` is scalar-only.** Every scalar field is included (`id` too); relation fields are excluded. A model's relations are therefore **not** persisted by `createModel` — write them with `linkRel` / `unlinkRel`. ## Typed relation link / unlink The generator emits a `RelationRef` const per relation (e.g. `Widget2Table.gadgetsRel`). Pass it to `linkRel` / `unlinkRel` for compile-time-checked relation writes. `targetIds` is a single id or a `List`. ```dart // link two gadgets to a widget await db.transact( db.txFor(Widget2Table()).linkRel('w1', Widget2Table.gadgetsRel, ['g1', 'g2']), ); // unlink one await db.transact( db.txFor(Widget2Table()).unlinkRel('w1', Widget2Table.gadgetsRel, 'g1'), ); ``` ## The `table.tx(db)` sugar Each generated table also emits a `tx(db)` method, so `table.tx(db)` is shorthand for `db.txFor(table)`: ```dart await db.transact( Widget2Table().tx(db).createModel( const Widget2(id: 'w1', name: 'Box', weight: 3, gadgets: []), ), ); ``` A model field literally named `tx` would collide with this method — a documented, extremely rare edge. ## Composing with untyped writes Because `db.transact` accepts any `ToTransaction`, typed writes mix freely with the untyped API in the same transaction: ```dart await db.transact( db.txFor(t).create() ..set(t.title, 'New') ..set(t.priority, 1), ); ``` ```dart // Typed write alongside untyped chunk await db.transact(db.txFor(t).update('t1')..set(t.priority, 2)); await db.transact(db.tx['todos']['t2'].update({'priority': 5})); ```