instantdb_flutter 0.2.5 copy "instantdb_flutter: ^0.2.5" to clipboard
instantdb_flutter: ^0.2.5 copied to clipboard

A real-time, offline-first database client for Flutter with reactive bindings.

InstantDB Flutter #

A real-time, offline-first database client for Flutter with reactive bindings. This package provides a Flutter/Dart port of InstantDB, enabling you to build real-time, collaborative applications with ease.

Features #

  • Real-time synchronization - Changes sync instantly across all connected clients with differential sync for reliable deletions
  • Offline-first - Local SQLite storage with automatic sync when online
  • Reactive UI - Widgets automatically update when data changes using Signals
  • Type-safe queries - InstaQL query language with schema validation
  • Transactions - Atomic operations with optimistic updates and rollback
  • Authentication - Built-in user authentication and session management
  • Presence system - Real-time collaboration features (cursors, typing, reactions, avatars) with consistent multi-instance synchronization
  • Conflict resolution - Automatic handling of concurrent data modifications
  • Flutter widgets - Purpose-built reactive widgets for common patterns

Quick Start #

1. Installation #

Add to your pubspec.yaml:

dependencies:
  instantdb_flutter: ^0.1.0

2. Initialize InstantDB #

import 'package:instantdb_flutter/instantdb_flutter.dart';

// Initialize your database
final db = await InstantDB.init(
  appId: 'your-app-id', // Get this from instantdb.com
  config: const InstantConfig(
    syncEnabled: true,
  ),
);

3. Define Your Schema (Optional) #

final todoSchema = Schema.object({
  'id': Schema.id(),
  'text': Schema.string(minLength: 1),
  'completed': Schema.boolean(),
  'createdAt': Schema.number(),
});

final schema = InstantSchemaBuilder()
  .addEntity('todos', todoSchema)
  .build();

4. Build Reactive UI #

class TodoList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return InstantProvider(
      db: db,
      child: InstantBuilderTyped<List<Map<String, dynamic>>>(
        query: {
          'todos': {
            'orderBy': {'createdAt': 'desc'},
          },
        },
        transformer: (data) => (data['todos'] as List).cast<Map<String, dynamic>>(),
        builder: (context, todos) {
          return ListView.builder(
            itemCount: todos.length,
            itemBuilder: (context, index) {
              final todo = todos[index];
              return ListTile(
                title: Text(todo['text']),
                leading: Checkbox(
                  value: todo['completed'],
                  onChanged: (value) => _toggleTodo(todo['id']),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

5. Perform Mutations #

// Create a new todo using the new tx namespace (recommended)
final todoId = db.id();
await db.transactChunk(
  db.tx['todos'][todoId].update({
    'text': 'Learn InstantDB Flutter',
    'completed': false,
    'createdAt': DateTime.now().millisecondsSinceEpoch,
  })
);

// Or use the traditional approach
await db.transact([
  ...db.create('todos', {
    'id': db.id(), // Generates a proper UUID - required by InstantDB
    'text': 'Learn InstantDB Flutter',
    'completed': false,
    'createdAt': DateTime.now().millisecondsSinceEpoch,
  }),
]);

// Update with the new tx API
await db.transactChunk(
  db.tx['todos'][todoId].update({'completed': true})
);

// Deep merge for nested updates
await db.transactChunk(
  db.tx['users'][userId].merge({
    'preferences': {
      'theme': 'dark',
      'notifications': {'email': false}
    }
  })
);

// Delete a todo
await db.transact([
  db.delete(todoId),
]);

Core Concepts #

Reactive Queries #

InstantDB Flutter uses Signals for reactivity. Queries return Signal<QueryResult> objects that automatically update when underlying data changes.

// Simple query with the new subscribeQuery alias (recommended)
final querySignal = db.subscribeQuery({
  'users': {
    'where': {'active': true},
  },
});

// One-time query (no subscriptions)
final result = await db.queryOnce({'users': {}});

// Advanced query with new operators
final advancedQuery = db.subscribeQuery({
  'users': {
    'where': {
      'age': {'\$gte': 18, '\$lt': 65}, // Age between 18-65
      'email': {'\$like': '%@company.com'}, // Company emails
      'status': {'\$ne': 'inactive'}, // Not inactive
    },
    'orderBy': {'createdAt': 'desc'},
    'limit': 20,
  },
});

// React to changes
Watch((context) {
  final result = querySignal.value;
  return Text('Users: ${result.data?['users']?.length ?? 0}');
});

Transactions #

All mutations happen within transactions, which provide atomicity and enable optimistic updates. Use the new tx namespace for cleaner, more intuitive syntax:

// New tx namespace API (recommended)
final postId = db.id();
await db.transactChunk(
  db.tx['users'][userId]
    .update({'name': 'New Name'})
    .link({'posts': [postId]})
);

await db.transactChunk(
  db.tx['posts'][postId].update({
    'title': 'Hello World',
    'authorId': lookup('users', 'email', 'john@example.com'),
  })
);

// Traditional approach still works
await db.transact([
  db.update(userId, {'name': 'New Name'}),
  ...db.create('posts', {
    'id': db.id(),
    'title': 'Hello World',
    'authorId': userId,
  }),
]);

Real-time Sync #

When sync is enabled, changes are automatically synchronized across all connected clients:

// Enable sync during initialization
final db = await InstantDB.init(
  appId: 'your-app-id',
  config: const InstantConfig(
    syncEnabled: true,
    verboseLogging: false, // Set to true for detailed debug output
  ),
);

// Monitor connection status
ConnectionStatusBuilder(
  builder: (context, isOnline) {
    return Icon(
      isOnline ? Icons.cloud_done : Icons.cloud_off,
    );
  },
)

Enhanced Sync Features

InstantDB Flutter includes advanced synchronization capabilities:

  • Differential Sync: Automatically detects and syncs deletions between instances
  • Deduplication Logic: Prevents duplicate entities during sync operations
  • Transaction Integrity: Proper conversion to InstantDB's tx-steps format
  • Comprehensive Logging: Built-in hierarchical logging for debugging sync issues

All CRUD operations (Create, Read, Update, Delete) sync reliably across multiple running instances, including edge cases like deleting the last entity in a collection.

Presence System #

InstantDB includes a real-time presence system for collaborative features. Use the new room-based API for better organization:

// Join a room to get a scoped API (recommended)
final room = db.presence.joinRoom('room-id', initialPresence: {
  'username': 'Alice',
  'status': 'online',
});

// Room-scoped operations
await room.updateCursor(x: 100, y: 200);
await room.setTyping(true);
await room.sendReaction('❤️', metadata: {'x': 100, 'y': 200});
await room.setPresence({'mood': 'happy'});

// Topic-based messaging within rooms
await room.publishTopic('chat', {'message': 'Hello everyone!'});
room.subscribeTopic('chat').listen((data) {
  print('New message: ${data['message']}');
});

// Listen to room updates
Watch((context) {
  final cursors = room.getCursors().value;
  final typingUsers = room.getTyping().value;
  final reactions = room.getReactions().value;
  final presence = room.getPresence().value;

  return YourCollaborativeWidget(
    cursors: cursors,
    typingUsers: typingUsers,
    reactions: reactions,
    presence: presence,
  );
});

// Direct API still works for simple use cases
await db.presence.updateCursor('room-id', x: 100, y: 200);
await db.presence.setTyping('room-id', true);
await db.presence.sendReaction('room-id', '❤️');
await db.presence.leaveRoom('room-id');

Widget Reference #

InstantProvider #

Provides InstantDB instance to the widget tree:

InstantProvider(
  db: db,
  child: MyApp(),
)

InstantBuilder #

Generic reactive query widget:

InstantBuilder(
  query: {'todos': {}},
  builder: (context, data) => TodoList(todos: data['todos']),
  loadingBuilder: (context) => CircularProgressIndicator(),
  errorBuilder: (context, error) => Text('Error: $error'),
)

InstantBuilderTyped #

Type-safe reactive query widget:

InstantBuilderTyped<List<Todo>>(
  query: {'todos': {}},
  transformer: (data) => Todo.fromList(data['todos']),
  builder: (context, todos) => TodoList(todos: todos),
)

AuthBuilder #

Reactive authentication state widget:

AuthBuilder(
  builder: (context, user) {
    if (user != null) {
      return WelcomeScreen(user: user);
    } else {
      return LoginScreen();
    }
  },
)

Query Language (InstaQL) #

InstantDB uses a declarative query language with advanced operators:

// Basic query
{'users': {}}

// Advanced operators
{
  'users': {
    'where': {
      // Comparison operators
      'age': {'\$gte': 18, '\$lt': 65},
      'salary': {'\$gt': 50000, '\$lte': 200000},
      'status': {'\$ne': 'inactive'},

      // String pattern matching
      'email': {'\$like': '%@company.com'},
      'name': {'\$ilike': '%john%'}, // Case insensitive

      // Array operations
      'tags': {'\$contains': 'vip'},
      'skills': {'\$size': {'\$gte': 3}},
      'roles': {'\$in': ['admin', 'moderator']},

      // Existence checks
      'profilePicture': {'\$exists': true},
      'deletedAt': {'\$isNull': true},

      // Logical operators
      '\$and': [
        {'age': {'\$gte': 18}},
        {'\$or': [
          {'department': 'engineering'},
          {'department': 'design'}
        ]}
      ]
    },
    'orderBy': {'createdAt': 'desc'},
    'limit': 10,
    'offset': 20,
  }
}

// With relationships and nested conditions
{
  'users': {
    'include': {
      'posts': {
        'where': {'published': true},
        'orderBy': {'createdAt': 'desc'},
        'limit': 5,
      },
      'profile': {}
    },
  }
}

// Lookup references (reference by attribute instead of ID)
{
  'posts': {
    'where': {
      'author': lookup('users', 'email', 'john@example.com')
    }
  }
}

Authentication #

InstantDB includes built-in authentication with convenient helper methods:

// Sign up
final user = await db.auth.signUp(
  email: 'user@example.com',
  password: 'password',
);

// Sign in
final user = await db.auth.signIn(
  email: 'user@example.com',
  password: 'password',
);

// Sign out
await db.auth.signOut();

// Get current auth state (new convenience methods)
final currentUser = db.getAuth(); // One-time check
final authSignal = db.subscribeAuth(); // Reactive updates

// Listen to auth state changes
db.auth.onAuthStateChange.listen((user) {
  if (user != null) {
    // User signed in
  } else {
    // User signed out
  }
});

// Use in reactive widgets
Watch((context) {
  final user = db.subscribeAuth().value;
  return user != null
    ? Text('Welcome ${user.email}')
    : Text('Please sign in');
});

Schema Validation #

Define and validate your data schemas:

final userSchema = Schema.object({
  'name': Schema.string(minLength: 1, maxLength: 100),
  'email': Schema.email(),
  'age': Schema.number(min: 0, max: 150),
  'posts': Schema.array(Schema.string()).optional(),
}, required: ['name', 'email']);

// Validate data
final isValid = userSchema.validate({
  'name': 'John Doe',
  'email': 'john@example.com',
  'age': 30,
});

Example App #

Check out the example todo app for a complete demonstration of:

  • Real-time synchronization between multiple instances
  • Offline functionality with local persistence
  • Reactive UI updates
  • CRUD operations with transactions

To run the example:

cd example
flutter pub get
flutter run

Web Platform Setup #

When deploying to web, you need to copy the SQLite web worker files to your web/ directory:

  1. Copy the required files from the example:

    cp example/web/sqflite_sw.js web/
    cp example/web/sqlite3.wasm web/
    
  2. Or manually download them from the sqflite_common_ffi_web package:

Without these files, you'll see an error: "An error occurred while initializing the web worker. This is likely due to a failure to find the worker javascript file at sqflite_sw.js"

Testing #

The package includes comprehensive tests. To run them:

flutter test

For integration testing with a real InstantDB instance, create a .env file:

INSTANTDB_API_ID=your-test-app-id

Architecture #

InstantDB Flutter is built on several key components:

  • Triple Store: Local SQLite-based storage using the RDF triple model (supports pattern queries like todos:*)
  • Query Engine: InstaQL parser and executor with reactive bindings using Signals
  • Sync Engine: WebSocket-based real-time synchronization with conflict resolution and differential sync
  • Transaction System: Atomic operations with optimistic updates and proper rollback handling
  • Reactive Layer: Signals-based reactivity for automatic UI updates
  • Cross-Platform: Uses SQLite for robust local storage on all Flutter platforms (iOS, Android, Web, macOS, Windows, Linux)

Logging and Debugging #

InstantDB Flutter includes comprehensive logging to help with development and debugging:

Configuration Options #

final db = await InstantDB.init(
  appId: 'your-app-id',
  config: const InstantConfig(
    syncEnabled: true,
    verboseLogging: true, // Enable detailed debug logging
  ),
);

Dynamic Log Level Control #

Update log levels at runtime for testing and debugging:

import 'package:logging/logging.dart';
import 'package:instantdb_flutter/src/core/logging_config.dart';

// Change log level dynamically
InstantDBLogging.updateLogLevel(Level.FINE);   // Verbose debugging
InstantDBLogging.updateLogLevel(Level.INFO);   // General information
InstantDBLogging.updateLogLevel(Level.WARNING); // Warnings and errors only

Log Levels #

  • Level.FINE - Detailed debug information (WebSocket messages, query execution, sync operations)
  • Level.INFO - General operational information (connections, transactions)
  • Level.WARNING - Important warnings and errors only (production-friendly)
  • Level.SEVERE - Critical errors only

Component-Specific Logging #

Enable logging for specific components:

// Set different log levels per component
InstantDBLogging.setLevel('sync', Level.FINE);        // Sync engine details
InstantDBLogging.setLevel('query', Level.INFO);       // Query operations
InstantDBLogging.setLevel('websocket', Level.WARNING); // WebSocket errors only
InstantDBLogging.setLevel('transaction', Level.FINE);  // Transaction details

Production Usage #

For production apps, use WARNING level to minimize console output while preserving error information:

final db = await InstantDB.init(
  appId: 'your-app-id',
  config: const InstantConfig(
    syncEnabled: true,
    verboseLogging: false, // Production-friendly logging
  ),
);

Performance Tips #

  1. Use specific queries: Avoid querying all data when you only need a subset
  2. Implement pagination: Use limit and offset for large datasets
  3. Cache management: The package automatically manages query caches
  4. Dispose resources: Properly dispose of InstantDB instances
  5. UUID Generation: Always use db.id() for entity IDs to ensure server compatibility
  6. Log level optimization: Use WARNING level in production to reduce console noise
// Good: Specific query with UUID
await db.transact([
  ...db.create('todos', {
    'id': db.id(), // Required UUID format
    'completed': false,
    'text': 'My todo',
  }),
]);

{'todos': {'where': {'completed': false}, 'limit': 20}}

// Avoid: Querying everything or custom IDs
{'todos': {}}

// Avoid: Custom string IDs (will cause server errors)
'id': 'my-custom-id' // ❌ Invalid - not a UUID

Contributing #

We welcome contributions! Please see our Contributing Guide for details.

License #

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments #

7
likes
150
points
616
downloads

Publisher

unverified uploader

Weekly Downloads

A real-time, offline-first database client for Flutter with reactive bindings.

Repository (GitHub)
Contributing

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

crypto, dio, flutter, flutter_dotenv, json_annotation, logging, path, signals_flutter, sqflite, sqflite_common_ffi_web, uuid, web_socket_channel

More

Packages that depend on instantdb_flutter