Skip to Content

Auth-Aware Kalam Client for Flutter

If your Flutter app uses expiring tokens and loads app state separately from raw auth, your Kalam client should follow the same lifecycle. Initialize the runtime once, connect when auth is ready, and only start subscriptions after the app user/session is ready.

This pattern keeps the SDK aligned with real app state and avoids a common failure mode: starting realtime work before your app knows which user it is operating as.

Use this sequence in production apps:

  1. Call KalamClient.init() once before runApp().
  2. Prime a shared client controller early so the SDK is ready to connect.
  3. Connect or refresh auth when the login state or JWT changes.
  4. Start subscriptions only after your app user/session has loaded.
  5. Dispose the client on logout or app shutdown.

This is especially useful when your app has two readiness stages:

  • Authentication is complete, so you can mint a JWT.
  • App session data is complete, so you know which subscriptions to start.

See also Authentication, Realtime Subscriptions, and Client Lifecycle.

Why not connect everything at startup?

KalamClient.init() belongs at startup. Full client connection usually does not.

Even with wsLazyConnect: true, a connect() call can still perform auth-related async work. If you await that before first render, you add boot latency. If you start subscriptions before your app user exists, you risk wiring realtime state to an incomplete session.

The safer pattern is:

  • initialize the SDK before runApp()
  • keep one shared client/controller alive while the app is open
  • react to auth and app-session changes with explicit lifecycle methods

Riverpod example

The example below uses Riverpod to split those stages cleanly.

import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:kalam_link/kalam_link.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await KalamClient.init(); runApp(const ProviderScope(child: MyApp())); } class AppUser { const AppUser({required this.id}); final String id; } final authTokenProvider = StreamProvider<String?>((ref) { return authRepository.idTokenChanges(); }); final appUserProvider = StreamProvider<AppUser?>((ref) { return userRepository.sessionChanges(); }); final kalamControllerProvider = Provider<KalamController>((ref) { final controller = KalamController(url: 'https://db.example.com'); ref.onDispose(controller.dispose); return controller; }); final kalamLifecycleProvider = Provider<void>((ref) { final controller = ref.watch(kalamControllerProvider); ref.listen<AsyncValue<String?>>(authTokenProvider, (_, next) { unawaited(controller.syncAuth(next.valueOrNull)); }); ref.listen<AsyncValue<AppUser?>>(appUserProvider, (_, next) { final user = next.valueOrNull; if (user == null) { unawaited(controller.stopRealtime()); return; } unawaited(controller.ensureRealtimeReady(userId: user.id)); }); }); class KalamController { KalamController({required this.url}); final String url; KalamClient? _client; String? _currentToken; String? _subscribedUserId; StreamSubscription<ChangeEvent>? _messagesSubscription; Future<void> syncAuth(String? token) async { if (token == _currentToken) { return; } _currentToken = token; if (token == null) { _subscribedUserId = null; await _messagesSubscription?.cancel(); _messagesSubscription = null; await _client?.dispose(); _client = null; return; } if (_client == null) { _client = await KalamClient.connect( url: url, wsLazyConnect: true, authProvider: () async => Auth.jwt(_currentToken!), ); return; } await _client!.refreshAuth(); } Future<void> ensureRealtimeReady({required String userId}) async { final client = _client; if (client == null || _subscribedUserId == userId) { return; } await _messagesSubscription?.cancel(); _subscribedUserId = userId; _messagesSubscription = client .subscribe( r'SELECT id, body, sent_at FROM messages WHERE recipient_id = $1', params: [userId], ) .listen((event) { switch (event) { case InsertEvent(:final row): debugPrint('new row: $row'); default: break; } }); } Future<void> stopRealtime() async { _subscribedUserId = null; await _messagesSubscription?.cancel(); _messagesSubscription = null; } Future<void> dispose() async { await _messagesSubscription?.cancel(); await _client?.dispose(); } }

Boot the lifecycle once

Create the controller once and let it react to auth/session changes for the lifetime of the app.

class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(kalamLifecycleProvider); return const Directionality( textDirection: TextDirection.ltr, child: Placeholder(), ); } }

This keeps the SDK lifecycle close to your app shell instead of scattering connect logic across screens.

Token refresh and auth changes

In most apps, your token can change after the client has already been created. When that happens:

  • return the latest token from authProvider
  • call refreshAuth() after the token changes
  • dispose the client entirely on logout

That gives the SDK one stable client instance while still letting credentials rotate.

await controller.syncAuth(freshToken);

Inside the controller above, the first authenticated state creates the client and later token changes call refreshAuth().

Common mistakes

  • Awaiting KalamClient.connect() before the first frame.
  • Creating a new client in every screen or feature provider.
  • Starting subscriptions before your app user/session has loaded.
  • Keeping subscriptions alive after logout.
  • Treating raw auth state and app session readiness as the same signal.

Where the SDK can reduce boilerplate further

This pattern works today, but it also shows where the Dart SDK could get more ergonomic:

  1. A small auth-aware client controller in the SDK that handles connect, refreshAuth, and dispose around token changes.
  2. A first-class setAuth() API so apps do not have to model token replacement indirectly.
  3. A lightweight cookbook package or example app using Riverpod for auth-driven lifecycle wiring.
  4. A subscription group helper that can start and stop a set of realtime queries as app session readiness changes.

If you are contributing to the SDK, these are high-leverage places to remove repetitive app code without hiding how Kalam auth and subscriptions work.

Last updated on