Skip to content

pillowsoft/instantdb_flutter

Repository files navigation

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');
});

Session Persistence

InstantDB supports automatic session persistence with intelligent storage backend selection.

Quick Start (No Setup Required)

final db = await InstantDB.init(
  appId: 'your-app-id',
  config: const InstantConfig(
    sessionStorageType: SessionStorageType.auto,  // Default
  ),
);

How it works:

  • Debug builds: Uses SharedPreferences (no macOS entitlements needed)
  • Release builds: Uses SecureStorage if available, falls back to SharedPreferences
  • Automatic: Works everywhere without any platform-specific setup

Storage Options

1. Auto (Default - Recommended)

sessionStorageType: SessionStorageType.auto
  • Debug: SharedPreferences (easy development)
  • Release: SecureStorage if configured, else SharedPreferences

2. Always SharedPreferences

sessionStorageType: SessionStorageType.sharedPreferences
  • Works everywhere without setup
  • Good enough for most apps

3. Always SecureStorage (Maximum Security)

sessionStorageType: SessionStorageType.secure
  • Requires adding dependency:
    dependencies:
      flutter_secure_storage: ^9.2.2
  • macOS: Requires keychain entitlements configuration
  • Throws error if dependency missing

4. Disabled

sessionStorageType: SessionStorageType.none
  • Disables session persistence completely

Migration from v0.2.9

Old API (Still works but deprecated):

config: InstantConfig(
  enableSessionPersistence: true,
)

New API:

config: InstantConfig(
  sessionStorageType: SessionStorageType.auto,
)

Platform Notes

  • macOS: Debug builds now work without entitlements!
  • iOS: Works with proper provisioning profiles
  • Android: Works out of box
  • Web: Always uses SharedPreferences
  • Windows/Linux: Works out of box

Schema Management

InstantDB Flutter includes a built-in CLI tool for managing your InstantDB schemas. The CLI wraps the official InstantDB CLI and provides seamless integration for Dart/Flutter projects.

Quick Start

# Pull schema from InstantDB cloud
dart run instantdb_flutter:schema pull

# Push schema to InstantDB cloud
dart run instantdb_flutter:schema push

# Show schema status
dart run instantdb_flutter:schema status

Prerequisites

  • Node.js and npx: InstantDB's official CLI requires Node.js
  • Authentication: Run npx instant-cli@latest login first
  • Environment: Set INSTANT_APP_ID or use --app-id flag

Available Commands

Command Description
pull Download schema from cloud → instant.schema.ts
push Upload instant.schema.ts to cloud
status Show local schema files and CLI availability
validate Validate Dart schema file syntax
diff Compare local vs cloud (coming soon)
help Show comprehensive help

Integration with Just/Make

Add to your justfile for convenient schema management:

schema-pull:
    dart run instantdb_flutter:schema pull

schema-push:
    dart run instantdb_flutter:schema push

schema-status:
    dart run instantdb_flutter:schema status

Then use: just schema-pull, just schema-push, etc.

Current Workflow

  1. Define schema in Dart (your application code)
  2. Create matching TypeScript schema (instant.schema.ts)
  3. Push to cloud: dart run instantdb_flutter:schema push
  4. Pull updates: dart run instantdb_flutter:schema pull

Note: Automatic Dart ↔ TypeScript conversion is planned for a future release. Currently, you need to manually maintain both formats.

Custom Schema Location

# Use custom Dart schema file location
dart run instantdb_flutter:schema pull -s lib/my_schema.dart
dart run instantdb_flutter:schema validate -s lib/my_schema.dart

Comprehensive Documentation

See SCHEMA_MANAGEMENT.md for:

  • Detailed command documentation
  • Integration patterns (CLI, justfile, npm scripts)
  • Best practices and workflows
  • Troubleshooting guide
  • Roadmap and planned features

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

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •