Flutter InstantDB
Real-time, offline-first database for Flutter with reactive bindings. Build collaborative apps in minutes.
- 📚 Docs: https://flutter-instantdb.vercel.app
- 🚀 Quick Start: https://flutter-instantdb.vercel.app/getting-started/quick-start
- 🧩 Example app: example/
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
Requirements
- Flutter SDK >= 3.8.0
- Dart SDK >= 3.8.0
- An InstantDB App ID (create one at instantdb.com)
Platform Support
| Platform | Support | Notes |
|---|---|---|
| Android | ✅ | SQLite storage |
| iOS | ✅ | SQLite storage |
| Web | ✅ | SQLite (WASM) persisted in IndexedDB |
| macOS | ✅ | SQLite storage |
| Windows | ✅ | SQLite storage |
| Linux | ✅ | SQLite storage |
Documentation
- Getting Started: Installation · Quick Start
- Concepts: Database · Schema
- API Reference: InstantDB · Queries · Transactions · Presence · Widgets · Types
- Authentication: Users · Sessions · Permissions
- Real-time: Sync · Presence · Collaboration
- Advanced: Offline · Performance · Migration · Troubleshooting
Quick Start
1. Installation
Add to your pubspec.yaml:
dependencies:
flutter_instantdb: ^1.1.2
Or install from the command line:
flutter pub add flutter_instantdb
2. Initialize InstantDB
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_instantdb/flutter_instantdb.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Load environment variables (optional, but recommended)
await dotenv.load(fileName: '.env');
// Initialize your database
final appId = dotenv.env['INSTANTDB_API_ID']!;
final db = await InstantDB.init(
appId: appId, // Or use your App ID directly: 'your-app-id'
config: InstantConfig(
syncEnabled: true, // Enable real-time sync
verboseLogging: true, // Enable debug logging in development
),
);
runApp(MyApp(db: db));
}
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
// Wrap your app with InstantProvider
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 TodosPage(),
),
);
}
}
// Access the database using InstantProvider.of(context)
class TodosPage extends StatelessWidget {
const TodosPage({super.key});
@override
Widget build(BuildContext context) {
return InstantBuilderTyped<List<Map<String, dynamic>>>(
query: {'todos': {}},
transformer: (data) {
final todos = (data['todos'] as List).cast<Map<String, dynamic>>();
// Sort client-side by createdAt in descending order
todos.sort((a, b) {
final aTime = a['createdAt'] as int? ?? 0;
final bTime = b['createdAt'] as int? ?? 0;
return bTime.compareTo(aTime);
});
return todos;
},
loadingBuilder: (context) => const Center(child: CircularProgressIndicator()),
errorBuilder: (context, error) => Center(child: Text('Error: $error')),
builder: (context, todos) {
if (todos.isEmpty) {
return const Center(child: Text('No todos yet'));
}
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo['text'] ?? ''),
leading: Checkbox(
value: todo['completed'] == true,
onChanged: (value) => _toggleTodo(context, todo),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _deleteTodo(context, todo['id']),
),
);
},
);
},
);
}
Future<void> _toggleTodo(BuildContext context, Map<String, dynamic> todo) async {
final db = InstantProvider.of(context);
await db.transact(
db.tx['todos'][todo['id']].update({'completed': !todo['completed']}),
);
}
Future<void> _deleteTodo(BuildContext context, String todoId) async {
final db = InstantProvider.of(context);
await db.transact(db.tx['todos'][todoId].delete());
}
}
5. Perform Mutations
Future<void> addTodo(BuildContext context, String text) async {
final db = InstantProvider.of(context);
// Create a new todo using the traditional transaction API
final todoId = db.id(); // Generates a proper UUID - required by InstantDB
await db.transact([
...db.create('todos', {
'id': todoId,
'text': text,
'completed': false,
'createdAt': DateTime.now().millisecondsSinceEpoch,
}),
]);
}
Future<void> toggleTodo(BuildContext context, Map<String, dynamic> todo) async {
final db = InstantProvider.of(context);
// Update using the tx namespace API (aligned with React InstantDB)
await db.transact(
db.tx['todos'][todo['id']].update({'completed': !todo['completed']}),
);
}
Future<void> deleteTodo(BuildContext context, String todoId) async {
final db = InstantProvider.of(context);
// Delete using the tx namespace API
await db.transact(db.tx['todos'][todoId].delete());
}
// Deep merge for nested updates
Future<void> updateUserPreferences(BuildContext context, String userId) async {
final db = InstantProvider.of(context);
await db.transact(
db.tx['users'][userId].merge({
'preferences': {
'theme': 'dark',
'notifications': {'email': false}
}
}),
);
}
Core Concepts
Reactive Queries
Flutter InstantDB uses Signals for reactivity. Use InstantBuilder widgets for reactive UI updates, or queryOnce for one-time data fetching.
// In a widget, use InstantBuilder for reactive queries
InstantBuilder(
query: {'todos': {}},
loadingBuilder: (context) => const Center(child: CircularProgressIndicator()),
errorBuilder: (context, error) => Center(child: Text('Error: $error')),
builder: (context, data) {
final todos = (data['todos'] as List? ?? []);
return Text('Total: ${todos.length} todos');
},
);
// One-time query (no subscriptions) - useful for operations like clearing all items
Future<void> clearAllTodos(BuildContext context) async {
final db = InstantProvider.of(context);
final queryResult = await db.queryOnce({'todos': {}});
if (queryResult.data != null && queryResult.data!['todos'] is List) {
final todos = (queryResult.data!['todos'] as List).cast<Map<String, dynamic>>();
for (final todo in todos) {
await db.transact(db.tx['todos'][todo['id']].delete());
}
}
}
// Using Watch widget for reactive updates with signals
Watch((context) {
final db = InstantProvider.of(context);
final cursors = room.getCursors().value;
return Text('Active cursors: ${cursors.length}');
});
Transactions
All mutations happen within transactions, which provide atomicity and enable optimistic updates. Access the database using InstantProvider.of(context):
// Using tx namespace API for updates and deletes (aligned with React InstantDB)
Future<void> toggleTodo(BuildContext context, Map<String, dynamic> todo) async {
final db = InstantProvider.of(context);
await db.transact(
db.tx['todos'][todo['id']].update({'completed': !todo['completed']}),
);
}
Future<void> deleteTodo(BuildContext context, String todoId) async {
final db = InstantProvider.of(context);
await db.transact(db.tx['todos'][todoId].delete());
}
// Using traditional API for creating new entities
Future<void> addTodo(BuildContext context, String text) async {
final db = InstantProvider.of(context);
await db.transact([
...db.create('todos', {
'id': db.id(), // Always use db.id() for proper UUID generation
'text': text,
'completed': false,
'createdAt': DateTime.now().millisecondsSinceEpoch,
}),
]);
}
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: appId,
config: InstantConfig(
syncEnabled: true, // Enable real-time sync
verboseLogging: true, // Enable debug logging in development
),
);
// Monitor connection status in your AppBar
ConnectionStatusBuilder(
builder: (context, isOnline) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isOnline ? Icons.cloud_done : Icons.cloud_off,
color: Colors.white70,
),
const SizedBox(width: 4),
Text(
isOnline ? 'Online' : 'Offline',
style: const TextStyle(fontSize: 12, color: Colors.white70),
),
],
);
},
)
Enhanced Sync Features
Flutter InstantDB 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 room-based API for better organization:
class CursorsPage extends StatefulWidget {
const CursorsPage({super.key});
@override
State<CursorsPage> createState() => _CursorsPageState();
}
class _CursorsPageState extends State<CursorsPage> {
String? _userId;
InstantRoom? _room;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_userId == null) {
_initializeUser();
_joinRoom();
}
}
void _initializeUser() {
final db = InstantProvider.of(context);
final currentUser = db.auth.currentUser.value;
_userId = currentUser?.id ?? db.getAnonymousUserId();
}
void _joinRoom() {
final db = InstantProvider.of(context);
// Join a room to get a scoped API
_room = db.presence.joinRoom('cursors-room');
}
void _updateCursor(Offset position) {
if (_room == null) return;
// Update cursor position using the room-based API
_room!.updateCursor(x: position.dx, y: position.dy);
}
void _removeCursor() {
_room?.removeCursor();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onHover: (event) => _updateCursor(event.localPosition),
onExit: (_) => _removeCursor(),
child: Stack(
children: [
// Display all cursors using Watch for reactivity
Watch((context) {
if (_room == null) return const SizedBox.shrink();
final cursors = _room!.getCursors().value;
return Stack(
children: cursors.entries.map((entry) {
final userId = entry.key;
final cursor = entry.value;
return Positioned(
left: cursor.x,
top: cursor.y,
child: CursorWidget(userId: userId, isMe: userId == _userId),
);
}).toList(),
);
}),
],
),
);
}
}
Typing Indicators:
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
@override
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
InstantRoom? _room;
Timer? _typingTimer;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final db = InstantProvider.of(context);
// Join room with initial presence
_room = db.presence.joinRoom(
'chat-room',
initialPresence: {'userName': 'Alice', 'status': 'online'},
);
}
void _startTyping() {
_typingTimer?.cancel();
_room?.setTyping(true);
// Auto-stop typing after 3 seconds of inactivity
_typingTimer = Timer(const Duration(seconds: 3), _stopTyping);
}
void _stopTyping() {
_room?.setTyping(false);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Typing indicators
Watch((context) {
if (_room == null) return const SizedBox.shrink();
final typingUsers = _room!.getTyping().value;
if (typingUsers.isEmpty) return const SizedBox.shrink();
return Text('${typingUsers.length} user(s) typing...');
}),
// Message input
TextField(
onChanged: (_) => _startTyping(),
onSubmitted: (_) => _stopTyping(),
),
],
);
}
}
Reactions:
void sendReaction(BuildContext context, String emoji, Offset position) {
final db = InstantProvider.of(context);
final room = db.presence.joinRoom('reactions-room');
// Send reaction with position metadata
room.sendReaction(
emoji,
metadata: {'x': position.dx, 'y': position.dy},
);
}
// Display reactions
Watch((context) {
if (room == null) return const SizedBox.shrink();
final reactions = room!.getReactions().value;
return Stack(
children: reactions.map((reaction) {
return Positioned(
left: (reaction.metadata?['x'] ?? 0.0).toDouble(),
top: (reaction.metadata?['y'] ?? 0.0).toDouble(),
child: Text(reaction.emoji, style: const TextStyle(fontSize: 32)),
);
}).toList(),
);
});
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 magic code (passwordless) and guest authentication:
class AuthPage extends StatefulWidget {
const AuthPage({super.key});
@override
State<AuthPage> createState() => _AuthPageState();
}
class _AuthPageState extends State<AuthPage> {
final _emailController = TextEditingController();
final _codeController = TextEditingController();
bool _codeSent = false;
String? _userEmail;
// Send magic code to email
Future<void> _sendMagicCode() async {
final email = _emailController.text.trim();
final db = InstantProvider.of(context);
await db.auth.sendMagicCode(email: email);
setState(() {
_codeSent = true;
_userEmail = email;
});
}
// Verify the magic code
Future<void> _verifyCode() async {
final code = _codeController.text.trim();
final db = InstantProvider.of(context);
await db.auth.verifyMagicCode(email: _userEmail!, code: code);
// User is now signed in!
}
// Sign in as guest
Future<void> _signAsGuest() async {
final db = InstantProvider.of(context);
await db.auth.signInAsGuest();
}
// Sign out
Future<void> _signOut() async {
final db = InstantProvider.of(context);
await db.auth.signOut();
}
@override
Widget build(BuildContext context) {
final db = InstantProvider.of(context);
// Listen to auth state changes with StreamBuilder
return StreamBuilder<AuthUser?>(
stream: db.auth.onAuthStateChange,
builder: (context, snapshot) {
final user = snapshot.data;
if (user != null) {
return Column(
children: [
Text('Signed in as ${user.email}'),
Text('User ID: ${user.id}'),
if (user.isGuest == true) Text('(Guest user)'),
ElevatedButton(
onPressed: _signOut,
child: const Text('Sign Out'),
),
],
);
}
if (_codeSent) {
return Column(
children: [
Text('Code sent to $_userEmail'),
TextField(
controller: _codeController,
decoration: const InputDecoration(labelText: 'Verification Code'),
),
ElevatedButton(
onPressed: _verifyCode,
child: const Text('Verify Code'),
),
],
);
}
return Column(
children: [
TextField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
),
ElevatedButton(
onPressed: _sendMagicCode,
child: const Text('Send Magic Code'),
),
ElevatedButton(
onPressed: _signAsGuest,
child: const Text('Continue as Guest'),
),
],
);
},
);
}
}
Getting Current User:
// One-time check
final db = InstantProvider.of(context);
final currentUser = db.auth.currentUser.value;
// For anonymous/guest users
final anonymousId = db.getAnonymousUserId();
// Verify refresh token
if (user?.refreshToken != null) {
await db.auth.verifyRefreshToken(refreshToken: user!.refreshToken!);
}
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:
-
Copy the required files from the example:
cp example/web/sqflite_sw.js web/ cp example/web/sqlite3.wasm web/ -
Or manually download them from the sqflite_common_ffi_web package:
- Follow the setup instructions at: https://github.com/tekartik/sqflite/tree/master/packages_web/sqflite_common_ffi_web#setup-binaries
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
Flutter InstantDB 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
Flutter InstantDB 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:flutter_instantdb/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
- Use specific queries: Avoid querying all data when you only need a subset
- Implement pagination: Use
limitandoffsetfor large datasets - Cache management: The package automatically manages query caches
- Dispose resources: Properly dispose of InstantDB instances
- UUID Generation: Always use
db.id()for entity IDs to ensure server compatibility - 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
- InstantDB - The original JavaScript implementation
- Signals Flutter - Reactive state management
- SQLite - Local data persistence