flutter_instantdb 1.1.2+1 copy "flutter_instantdb: ^1.1.2+1" to clipboard
flutter_instantdb: ^1.1.2+1 copied to clipboard

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

Flutter InstantDB #

Real-time, offline-first database for Flutter with reactive bindings. Build collaborative apps in minutes.

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 #

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:

  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 #

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 #

  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 #

1
likes
150
points
11
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

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

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

args, 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 flutter_instantdb