flutter_convex_overview.md

Convex Flutter

Home Screen Messaging Screen
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:

bash
1flutter 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.

PlatformConfiguration 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:

xml
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:

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:

typescript
1// convex/health.ts
2import { query } from "./_generated/server";
3
4export 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

dart
1import 'package:convex_flutter/convex_flutter.dart';
2
3Future<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
20void 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:

dart
1// Set authentication
2await client.setAuth(token: 'your-jwt-token');
3
4// Clear authentication
5await client.setAuth(token: null);

Automatic Token Refresh (Recommended)

For production apps, use setAuthWithRefresh which automatically refreshes tokens 60 seconds before they expire:

dart
1final 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
12authHandle.dispose();

Auth State Stream

Listen to authentication state changes reactively:

dart
1client.authState.listen((isAuthenticated) {
2 setState(() => _isLoggedIn = isAuthenticated);
3});

Sync Auth Check

Check current auth state synchronously:

dart
1if (client.isAuthenticated) {
2 // User is authenticated
3}

Clear Authentication

Clear auth and stop any running token refresh:

dart
1await 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):

dart
1await 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
9try {
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:

dart
1// Listen to connection state changes
2ConvexClient.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
16StreamBuilder<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
32if (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):

typescript
1// convex/health.ts
2import { query } from "./_generated/server";
3
4export const ping = query({
5 args: {},
6 handler: async () => {
7 return "ok";
8 },
9});

2. Trigger it on app startup:

dart
1// In your home screen or app initialization
2@override
3void 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:

dart
1// Use any existing query to trigger connection
2ConvexClient.instance.query('users:list', {'limit': '1'});

Manual Connection Check (Deprecated)

For backward compatibility, you can check connection status manually using a health check query:

dart
1// Configure a lightweight query for health checks
2await 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)
10final status = await ConvexClient.instance.checkConnection();
11
12switch (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:

dart
1final connected = await ConvexClient.instance.reconnect();
2if (connected) {
3 print('Reconnected successfully');
4}

Lifecycle Monitoring

Monitor app lifecycle events to handle foreground/background transitions.

Listen to Lifecycle Events

dart
1ConvexClient.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 foreground
  • AppLifecycleEvent.paused - App in background
  • AppLifecycleEvent.inactive - App inactive (e.g., during phone call)
  • AppLifecycleEvent.detached - App being terminated

API overview

MethodDescription
ConvexClient.initialize(ConvexConfig)Initialize singleton client with configuration
ConvexClient.instanceAccess 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
authStateStream of auth state changes (Stream<bool>)
isAuthenticatedCurrent auth state (sync getter)
clearAuth()Clear auth and stop token refresh
connectionStateReal-time WebSocket connection state stream (Stream<WebSocketConnectionState>)
currentConnectionStateCurrent connection state (sync getter)
isConnectedReturns true if WebSocket is connected (sync getter)
checkConnection()(Deprecated) Manually check connection status, returns ConnectionStatus
reconnect()Manually trigger reconnection attempt, returns bool
lifecycleEventsStream 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/:

1cd example
2flutter 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
      1curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    • Update your PATH (add to ~/.bashrc, ~/.zshrc, or equivalent):
      bash
      1source "$HOME/.cargo/env"
    • Verify installation:
      bash
      1rustc --version
      2cargo --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 install inside the example/ios or your app's ios folder 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 deploymentUrl and network permissions
  • Timeout errors: Increase operationTimeout in ConvexConfig

📖 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.