flutter_convex_overview.md
Convex Flutter
![]() |
![]() |
| Home Screen | Real-time Messaging |
A Flutter plugin for integrating with the Convex backend. It provides a simple Dart API over the Convex Rust core to run queries, mutations, and actions, and to subscribe to real-time updates.
This package wraps the Convex Rust library and exposes a Flutter-friendly interface.
Features
- Real-time subscriptions to Convex queries
- Simple Dart API for queries, mutations, and actions
- Authentication with automatic token refresh
- Auth state stream for reactive UI updates
- WebSocket connection state - Real-time connection status monitoring via streams
- Operation timeouts - Configurable timeout for all queries, mutations, and actions
- Lifecycle monitoring - Stream of app lifecycle events (foreground/background)
- Connection management - Manual connection checking and reconnect functionality
- Singleton pattern - Access client anywhere via
ConvexClient.instance - Multi-platform support - Works on Web (pure Dart), Android, iOS, macOS, Windows, and Linux (FFI)
Installation
Add the package to your Flutter project:
| 1 | flutter pub add convex_flutter |
That's it! The health check query mentioned below is optional - you can start using the SDK immediately without it.
Platform Configuration
Important: Some platforms require additional configuration for network access. This is a one-time setup.
| Platform | Configuration Required |
|---|---|
| Web | ✅ None - works automatically |
| iOS | ✅ None - works automatically |
| macOS | ⚠️ Network entitlements required |
| Android | ⚠️ INTERNET permission required |
| Windows | ✅ None - works automatically |
| Linux | ✅ None - works automatically |
Quick Setup
macOS: Add network entitlements to macos/Runner/DebugProfile.entitlements and Release.entitlements:
| 1 | <key>com.apple.security.network.client</key> |
| 2 | <true/> |
| 3 | <key>com.apple.security.network.server</key> |
| 4 | <true/> |
Android: Add internet permission to android/app/src/main/AndroidManifest.xml:
| 1 | <uses-permission android:name="android.permission.INTERNET" /> |
📖 See PLATFORM_CONFIGURATION.md for complete setup instructions and troubleshooting.
Requirements
- Dart SDK >= 3.8.1 and Flutter >= 3.3.0
- Web platform: No additional requirements (uses pure Dart implementation)
- Native platforms (Android, iOS, macOS, Windows, Linux):
- Rust toolchain (rustup + cargo) for building native code
- Platform-specific toolchains:
- Android: JDK 11 and Android SDK/NDK
- iOS/macOS: Xcode and CocoaPods
- Windows: Visual Studio Build Tools (C++)
- Linux: clang, pkg-config, and build essentials
Quick start
Optional: Create a Health Check Query (Recommended)
For connection monitoring and health checks, it's recommended to create a lightweight health check query in your Convex backend. This is optional but provides a clean way to verify connectivity without side effects.
Create a file convex/health.ts in your Convex backend:
| 1 | // convex/health.ts |
| 2 | import { query } from "./_generated/server"; |
| 3 | |
| 4 | export const ping = query({ |
| 5 | args: {}, |
| 6 | handler: async () => { |
| 7 | return "ok"; |
| 8 | }, |
| 9 | }); |
This creates a lightweight endpoint at health:ping that you can use for connection health checks. It has no side effects and returns instantly.
Initialize the Client
| 1 | import 'package:convex_flutter/convex_flutter.dart'; |
| 2 | |
| 3 | Future<void> main() async { |
| 4 | WidgetsFlutterBinding.ensureInitialized(); |
| 5 | |
| 6 | // Initialize the client once (singleton) |
| 7 | await ConvexClient.initialize( |
| 8 | ConvexConfig( |
| 9 | deploymentUrl: 'https://my-app.convex.cloud', |
| 10 | clientId: 'flutter-app-1.0', |
| 11 | operationTimeout: Duration(seconds: 30), // Optional, defaults to 30s |
| 12 | healthCheckQuery: 'health:ping', // Optional, for connection checks (requires health.ts) |
| 13 | ), |
| 14 | ); |
| 15 | |
| 16 | runApp(MyApp()); |
| 17 | } |
| 18 | |
| 19 | // Access the client anywhere in your app |
| 20 | void example() async { |
| 21 | final client = ConvexClient.instance; |
| 22 | |
| 23 | // Optional: authenticate (see Authentication section below) |
| 24 | await client.setAuth(token: 'YOUR_AUTH_TOKEN'); |
| 25 | |
| 26 | // Query (with timeout) |
| 27 | try { |
| 28 | final users = await client.query('users:list', {'limit': '10'}); |
| 29 | print('Users: $users'); |
| 30 | } on TimeoutException { |
| 31 | print('Connection timeout!'); |
| 32 | } |
| 33 | |
| 34 | // Subscribe to real-time updates |
| 35 | final sub = await client.subscribe( |
| 36 | name: 'messages:list', |
| 37 | args: {}, |
| 38 | onUpdate: (value) => print('Update: $value'), |
| 39 | onError: (message, value) => print('Error: $message ${value ?? ''}'), |
| 40 | ); |
| 41 | |
| 42 | // Mutation |
| 43 | await client.mutation( |
| 44 | name: 'messages:send', |
| 45 | args: {'body': 'Hello!', 'author': 'User123'}, |
| 46 | ); |
| 47 | |
| 48 | // Action (if you have actions defined) |
| 49 | // final res = await client.action(name: 'files:upload', args: {...}); |
| 50 | |
| 51 | // Later, when done |
| 52 | sub.cancel(); |
| 53 | } |
Authentication
The SDK provides comprehensive authentication support for Convex backends.
Simple Token Authentication
For basic scenarios or testing, set a static JWT token:
| 1 | // Set authentication |
| 2 | await client.setAuth(token: 'your-jwt-token'); |
| 3 | |
| 4 | // Clear authentication |
| 5 | await client.setAuth(token: null); |
Automatic Token Refresh (Recommended)
For production apps, use setAuthWithRefresh which automatically refreshes tokens 60 seconds before they expire:
| 1 | final authHandle = await client.setAuthWithRefresh( |
| 2 | fetchToken: () async { |
| 3 | // Return JWT from your auth provider (Firebase, Clerk, Auth0, etc.) |
| 4 | return await FirebaseAuth.instance.currentUser?.getIdToken(); |
| 5 | }, |
| 6 | onAuthChange: (isAuthenticated) { |
| 7 | print('Auth state: $isAuthenticated'); |
| 8 | }, |
| 9 | ); |
| 10 | |
| 11 | // When signing out, dispose the auth handle |
| 12 | authHandle.dispose(); |
Auth State Stream
Listen to authentication state changes reactively:
| 1 | client.authState.listen((isAuthenticated) { |
| 2 | setState(() => _isLoggedIn = isAuthenticated); |
| 3 | }); |
Sync Auth Check
Check current auth state synchronously:
| 1 | if (client.isAuthenticated) { |
| 2 | // User is authenticated |
| 3 | } |
Clear Authentication
Clear auth and stop any running token refresh:
| 1 | await client.clearAuth(); |
Connection Management
The SDK provides tools for managing connection state and handling network interruptions.
Operation Timeouts
All queries, mutations, and actions have configurable timeouts (default: 30 seconds):
| 1 | await ConvexClient.initialize( |
| 2 | ConvexConfig( |
| 3 | deploymentUrl: 'https://my-app.convex.cloud', |
| 4 | operationTimeout: Duration(seconds: 45), // Custom timeout |
| 5 | ), |
| 6 | ); |
| 7 | |
| 8 | // Operations will throw TimeoutException if they exceed the timeout |
| 9 | try { |
| 10 | await ConvexClient.instance.query('slowQuery', {}); |
| 11 | } on TimeoutException { |
| 12 | print('Operation timed out!'); |
| 13 | } |
Real-Time WebSocket Connection State (Recommended)
Monitor WebSocket connection state in real-time using streams. This is the recommended approach for connection monitoring:
| 1 | // Listen to connection state changes |
| 2 | ConvexClient.instance.connectionState.listen((state) { |
| 3 | switch (state) { |
| 4 | case WebSocketConnectionState.connected: |
| 5 | print('WebSocket connected!'); |
| 6 | // Update UI, enable features |
| 7 | break; |
| 8 | case WebSocketConnectionState.connecting: |
| 9 | print('WebSocket connecting...'); |
| 10 | // Show loading indicator |
| 11 | break; |
| 12 | } |
| 13 | }); |
| 14 | |
| 15 | // Or use in a StreamBuilder for reactive UI |
| 16 | StreamBuilder<WebSocketConnectionState>( |
| 17 | stream: ConvexClient.instance.connectionState, |
| 18 | initialData: ConvexClient.instance.currentConnectionState, |
| 19 | builder: (context, snapshot) { |
| 20 | final state = snapshot.data ?? WebSocketConnectionState.connecting; |
| 21 | final isConnected = state == WebSocketConnectionState.connected; |
| 22 | |
| 23 | return Chip( |
| 24 | avatar: Icon(isConnected ? Icons.cloud_done : Icons.cloud_sync), |
| 25 | label: Text(isConnected ? 'Connected' : 'Connecting'), |
| 26 | backgroundColor: isConnected ? Colors.green : Colors.orange, |
| 27 | ); |
| 28 | }, |
| 29 | ) |
| 30 | |
| 31 | // Synchronous access to current state |
| 32 | if (ConvexClient.instance.isConnected) { |
| 33 | // WebSocket is connected |
| 34 | } |
Features:
- Real-time state updates via Stream (no polling needed)
- Automatic state transitions when WebSocket connects/disconnects
- Synchronous getter for immediate state access
- Works across all platforms
Note: The WebSocket connection is established lazily when the first operation (query, mutation, subscribe, action) is executed.
Optional: Auto-Connect on Startup
To establish the connection immediately when your app starts (recommended for better UX), trigger a lightweight query in your app's initialization. Using a dedicated health check query is the cleanest approach:
1. Create a health check query in your Convex backend (optional but recommended):
| 1 | // convex/health.ts |
| 2 | import { query } from "./_generated/server"; |
| 3 | |
| 4 | export const ping = query({ |
| 5 | args: {}, |
| 6 | handler: async () => { |
| 7 | return "ok"; |
| 8 | }, |
| 9 | }); |
2. Trigger it on app startup:
| 1 | // In your home screen or app initialization |
| 2 | @override |
| 3 | void initState() { |
| 4 | super.initState(); |
| 5 | // Trigger connection immediately with health check |
| 6 | ConvexClient.instance.query('health:ping', {}); |
| 7 | } |
Alternative: You can use any existing lightweight query instead of creating a dedicated health check:
| 1 | // Use any existing query to trigger connection |
| 2 | ConvexClient.instance.query('users:list', {'limit': '1'}); |
Manual Connection Check (Deprecated)
For backward compatibility, you can check connection status manually using a health check query:
| 1 | // Configure a lightweight query for health checks |
| 2 | await ConvexClient.initialize( |
| 3 | ConvexConfig( |
| 4 | deploymentUrl: 'https://my-app.convex.cloud', |
| 5 | healthCheckQuery: 'health:ping', // Lightweight health check query |
| 6 | ), |
| 7 | ); |
| 8 | |
| 9 | // Check connection status (deprecated - use connectionState stream instead) |
| 10 | final status = await ConvexClient.instance.checkConnection(); |
| 11 | |
| 12 | switch (status) { |
| 13 | case ConnectionStatus.connected: |
| 14 | print('Connected!'); |
| 15 | case ConnectionStatus.timeout: |
| 16 | print('Connection timeout'); |
| 17 | case ConnectionStatus.error: |
| 18 | print('Connection error'); |
| 19 | case ConnectionStatus.unknown: |
| 20 | print('Not checked yet'); |
| 21 | } |
Manual Reconnect
Trigger reconnection attempt manually:
| 1 | final connected = await ConvexClient.instance.reconnect(); |
| 2 | if (connected) { |
| 3 | print('Reconnected successfully'); |
| 4 | } |
Lifecycle Monitoring
Monitor app lifecycle events to handle foreground/background transitions.
Listen to Lifecycle Events
| 1 | ConvexClient.instance.lifecycleEvents.listen((event) { |
| 2 | print('App lifecycle: $event'); |
| 3 | |
| 4 | if (event == AppLifecycleEvent.resumed) { |
| 5 | // App came to foreground |
| 6 | // Optionally reconnect or refresh data |
| 7 | ConvexClient.instance.reconnect(); |
| 8 | } |
| 9 | |
| 10 | if (event == AppLifecycleEvent.paused) { |
| 11 | // App went to background |
| 12 | // Optionally pause polling or save state |
| 13 | } |
| 14 | }); |
Lifecycle Events
AppLifecycleEvent.resumed- App in foregroundAppLifecycleEvent.paused- App in backgroundAppLifecycleEvent.inactive- App inactive (e.g., during phone call)AppLifecycleEvent.detached- App being terminated
API overview
| Method | Description |
|---|---|
ConvexClient.initialize(ConvexConfig) | Initialize singleton client with configuration |
ConvexClient.instance | Access singleton instance anywhere |
query(name, args) | Execute a query with timeout, returns JSON string |
mutation({ name, args }) | Execute a mutation with timeout, returns JSON string |
action({ name, args }) | Execute an action with timeout, returns JSON string |
subscribe({ name, args, onUpdate, onError }) | Subscribe to real-time updates, returns SubscriptionHandle |
setAuth({ token }) | Set or clear static auth token |
setAuthWithRefresh({ fetchToken, onAuthChange }) | Set auth with automatic token refresh, returns AuthHandleWrapper |
authState | Stream of auth state changes (Stream<bool>) |
isAuthenticated | Current auth state (sync getter) |
clearAuth() | Clear auth and stop token refresh |
connectionState | Real-time WebSocket connection state stream (Stream<WebSocketConnectionState>) |
currentConnectionState | Current connection state (sync getter) |
isConnected | Returns true if WebSocket is connected (sync getter) |
checkConnection() | (Deprecated) Manually check connection status, returns ConnectionStatus |
reconnect() | Manually trigger reconnection attempt, returns bool |
lifecycleEvents | Stream of app lifecycle events (Stream<AppLifecycleEvent>) |
dispose() | Clean up client resources |
See the inline docs in lib/src/convex_client.dart for details.
Example app
An example is provided under example/:
| 1 | cd example |
| 2 | flutter run |
The example demonstrates:
- Real-time chat with subscriptions
- Sending messages with mutations
- Authentication with JWT tokens
- Auth state management
- WebSocket connection state monitoring with visual indicators
- Lifecycle event monitoring (shows app state in AppBar)
- Connection screen with real-time state history
- Automatic connection on app startup
- Singleton pattern usage (
ConvexClient.instance)
Troubleshooting
Build Issues
- Rust not found (native platforms only):
- Visit Rust Getting Started Guide
- Install Rust:bash
1 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Update your PATH (add to
~/.bashrc,~/.zshrc, or equivalent):bash1 source "$HOME/.cargo/env" - Verify installation:bash
1 rustc --version 2 cargo --version - Note: Not needed for web platform
- Android build issues: Use JDK 11, ensure NDK is installed via Android SDK Manager
- iOS/macOS: Run
pod installinside theexample/iosor your app'siosfolder if needed - Windows: Install Visual Studio Build Tools with C++ workload
Connection Issues
- macOS stuck in "connecting" state: Missing network entitlements - see PLATFORM_CONFIGURATION.md
- Android network errors: Missing INTERNET permission - see PLATFORM_CONFIGURATION.md
- WebSocket not connecting: Check your
deploymentUrland network permissions - Timeout errors: Increase
operationTimeoutinConvexConfig
📖 For detailed troubleshooting, see PLATFORM_CONFIGURATION.md
Contributing
Contributions are welcome! Please open an issue or pull request.
License
This project is licensed under the MIT License - see the LICENSE file for details.

