Skip to Content
API ReferenceTransactions

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:

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:

// 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.

Future<TransactionResult> transact(dynamic transaction)

Parameters:

  • transaction: Either List<Operation> or TransactionChunk

Returns: Future<TransactionResult>

await db.transact([ ...db.create('posts', { 'id': db.id(), 'title': 'Hello World', 'content': 'My first post', }), db.update(userId, {'lastPostAt': DateTime.now().millisecondsSinceEpoch}), ]);

TransactionResult

Result object returned by transaction operations.

class TransactionResult { final bool success; final String? error; final Map<String, dynamic>? data; }

Properties:

  • success (bool): Whether the transaction succeeded
  • error (String?): Error message if transaction failed
  • data (Map<String, dynamic>?): Additional result data

Operation Types

Create Operations

create()

Create a new entity.

List<Operation> create(String entityType, Map<String, dynamic> data)

Parameters:

  • entityType (String): Type of entity to create
  • data (Map<String, dynamic>): Entity data (must include id)

Returns: List<Operation> - List containing the create operation

Examples:

// 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.

Operation update(String entityId, Map<String, dynamic> data)

Parameters:

  • entityId (String): ID of entity to update
  • data (Map<String, dynamic>): Data to update

Returns: Operation - The update operation

Examples:

// 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.

Operation merge(String entityId, Map<String, dynamic> data)

Parameters:

  • entityId (String): ID of entity to merge into
  • data (Map<String, dynamic>): Data to deep merge

Returns: Operation - The merge operation

Examples:

// 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.

Operation delete(String entityId)

Parameters:

  • entityId (String): ID of entity to delete

Returns: Operation - The delete operation

Examples:

// 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<String, dynamic>; 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

Create a relationship between entities.

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

Remove a relationship between entities.

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:

// 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), ]);

New Transaction API (tx namespace)

TransactionNamespace

The new fluent transaction API provides a more intuitive way to build complex operations.

TransactionNamespace get tx

Access pattern:

db.tx[entityType][entityId].method(data)

Fluent Operations

update()

TransactionChunk update(Map<String, dynamic> data)

Example:

await db.transact( db.tx['users'][userId].update({ 'name': 'New Name', 'updatedAt': DateTime.now().millisecondsSinceEpoch, }) );

merge()

TransactionChunk merge(Map<String, dynamic> data)

Example:

await db.transact( db.tx['users'][userId].merge({ 'preferences': { 'theme': 'dark', 'notifications': {'email': false}, }, }) );
TransactionChunk link(Map<String, List<String>> links)

Example:

await db.transact( db.tx['users'][userId].link({ 'posts': [postId1, postId2], 'groups': [groupId], }) );
TransactionChunk unlink(Map<String, List<String>> links)

Example:

await db.transact( db.tx['users'][userId].unlink({ 'posts': [oldPostId], }) );

Chaining Operations

Chain multiple operations on the same entity:

await db.transact( db.tx['users'][userId] .update({'name': 'New Name'}) .merge({'preferences': {'theme': 'dark'}}) .link({'groups': [groupId]}) );

Complex Transaction Examples

// 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:

// 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<String, dynamic>; // 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:

class BatchProcessor { final InstantDB db; static const int batchSize = 50; BatchProcessor(this.db); Future<void> processBatch(List<Operation> 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 = <Operation>[]; // 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:

class TransactionValidator { static void validateTodo(Map<String, dynamic> 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<String, dynamic> 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<void> 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; } } }
await db.transact([ db.update(entityId, { 'updatedAt': DateTime.now().millisecondsSinceEpoch, }), ]);

Error Handling

Handle transaction errors appropriately:

Future<void> safeTransaction(List<Operation> 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:

// ✅ 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', }), ]);

Batch related operations in single transactions:

// ✅ 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:

// Validate data structure and constraints void validateBeforeCreate(Map<String, dynamic> 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:

Future<void> optimisticUpdate(String entityId, Map<String, dynamic> 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: