convex_flutter

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:

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:

<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>

Android: Add internet permission to android/app/src/main/AndroidManifest.xml:

<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

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:

// convex/health.ts
import { query } from "./_generated/server";

export const ping = query({
  args: {},
  handler: async () => {
    return "ok";
  },
});

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

import 'package:convex_flutter/convex_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize the client once (singleton)
  await ConvexClient.initialize(
    ConvexConfig(
      deploymentUrl: 'https://my-app.convex.cloud',
      clientId: 'flutter-app-1.0',
      operationTimeout: Duration(seconds: 30), // Optional, defaults to 30s
      healthCheckQuery: 'health:ping', // Optional, for connection checks (requires health.ts)
    ),
  );

  runApp(MyApp());
}

// Access the client anywhere in your app
void example() async {
  final client = ConvexClient.instance;

  // Optional: authenticate (see Authentication section below)
  await client.setAuth(token: 'YOUR_AUTH_TOKEN');

  // Query (with timeout)
  try {
    final users = await client.query('users:list', {'limit': '10'});
    print('Users: $users');
  } on TimeoutException {
    print('Connection timeout!');
  }

  // Subscribe to real-time updates
  final sub = await client.subscribe(
    name: 'messages:list',
    args: {},
    onUpdate: (value) => print('Update: $value'),
    onError: (message, value) => print('Error: $message ${value ?? ''}'),
  );

  // Mutation
  await client.mutation(
    name: 'messages:send',
    args: {'body': 'Hello!', 'author': 'User123'},
  );

  // Action (if you have actions defined)
  // final res = await client.action(name: 'files:upload', args: {...});

  // Later, when done
  sub.cancel();
}

Authentication

The SDK provides comprehensive authentication support for Convex backends.

Simple Token Authentication

For basic scenarios or testing, set a static JWT token:

// Set authentication
await client.setAuth(token: 'your-jwt-token');

// Clear authentication
await client.setAuth(token: null);

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

final authHandle = await client.setAuthWithRefresh(
  fetchToken: () async {
    // Return JWT from your auth provider (Firebase, Clerk, Auth0, etc.)
    return await FirebaseAuth.instance.currentUser?.getIdToken();
  },
  onAuthChange: (isAuthenticated) {
    print('Auth state: $isAuthenticated');
  },
);

// When signing out, dispose the auth handle
authHandle.dispose();

Auth State Stream

Listen to authentication state changes reactively:

client.authState.listen((isAuthenticated) {
  setState(() => _isLoggedIn = isAuthenticated);
});

Sync Auth Check

Check current auth state synchronously:

if (client.isAuthenticated) {
  // User is authenticated
}

Clear Authentication

Clear auth and stop any running token refresh:

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

await ConvexClient.initialize(
  ConvexConfig(
    deploymentUrl: 'https://my-app.convex.cloud',
    operationTimeout: Duration(seconds: 45), // Custom timeout
  ),
);

// Operations will throw TimeoutException if they exceed the timeout
try {
  await ConvexClient.instance.query('slowQuery', {});
} on TimeoutException {
  print('Operation timed out!');
}

Monitor WebSocket connection state in real-time using streams. This is the recommended approach for connection monitoring:

// Listen to connection state changes
ConvexClient.instance.connectionState.listen((state) {
  switch (state) {
    case WebSocketConnectionState.connected:
      print('WebSocket connected!');
      // Update UI, enable features
      break;
    case WebSocketConnectionState.connecting:
      print('WebSocket connecting...');
      // Show loading indicator
      break;
  }
});

// Or use in a StreamBuilder for reactive UI
StreamBuilder<WebSocketConnectionState>(
  stream: ConvexClient.instance.connectionState,
  initialData: ConvexClient.instance.currentConnectionState,
  builder: (context, snapshot) {
    final state = snapshot.data ?? WebSocketConnectionState.connecting;
    final isConnected = state == WebSocketConnectionState.connected;

    return Chip(
      avatar: Icon(isConnected ? Icons.cloud_done : Icons.cloud_sync),
      label: Text(isConnected ? 'Connected' : 'Connecting'),
      backgroundColor: isConnected ? Colors.green : Colors.orange,
    );
  },
)

// Synchronous access to current state
if (ConvexClient.instance.isConnected) {
  // WebSocket is connected
}

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

// convex/health.ts
import { query } from "./_generated/server";

export const ping = query({
  args: {},
  handler: async () => {
    return "ok";
  },
});

2. Trigger it on app startup:

// In your home screen or app initialization
@override
void initState() {
  super.initState();
  // Trigger connection immediately with health check
  ConvexClient.instance.query('health:ping', {});
}

Alternative: You can use any existing lightweight query instead of creating a dedicated health check:

// Use any existing query to trigger connection
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:

// Configure a lightweight query for health checks
await ConvexClient.initialize(
  ConvexConfig(
    deploymentUrl: 'https://my-app.convex.cloud',
    healthCheckQuery: 'health:ping', // Lightweight health check query
  ),
);

// Check connection status (deprecated - use connectionState stream instead)
final status = await ConvexClient.instance.checkConnection();

switch (status) {
  case ConnectionStatus.connected:
    print('Connected!');
  case ConnectionStatus.timeout:
    print('Connection timeout');
  case ConnectionStatus.error:
    print('Connection error');
  case ConnectionStatus.unknown:
    print('Not checked yet');
}

Manual Reconnect

Trigger reconnection attempt manually:

final connected = await ConvexClient.instance.reconnect();
if (connected) {
  print('Reconnected successfully');
}

Lifecycle Monitoring

Monitor app lifecycle events to handle foreground/background transitions.

Listen to Lifecycle Events

ConvexClient.instance.lifecycleEvents.listen((event) {
  print('App lifecycle: $event');

  if (event == AppLifecycleEvent.resumed) {
    // App came to foreground
    // Optionally reconnect or refresh data
    ConvexClient.instance.reconnect();
  }

  if (event == AppLifecycleEvent.paused) {
    // App went to background
    // Optionally pause polling or save state
  }
});

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

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

cd example
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:
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
      
    • Update your PATH (add to ~/.bashrc, ~/.zshrc, or equivalent):
      source "$HOME/.cargo/env"
      
    • Verify installation:
      rustc --version
      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 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.

Libraries

convex_flutter
convex_flutter_web
Web platform implementation of convex_flutter plugin.