clotpulse_flutter

pub.dev

Official Flutter SDK for ClotPulse — self-hosted realtime messaging platform.
Works on Android, iOS, and Flutter Web.

📦 pub.dev/packages/clotpulse_flutter


Installation

# pubspec.yaml
dependencies:
  clotpulse_flutter: ^2.0.0
flutter pub get

Android

Add to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET"/>

iOS

No extra config needed for HTTPS. For local dev over HTTP, add to ios/Runner/Info.plist:

<key>NSAppTransportSecurity</key>
<dict><key>NSAllowsArbitraryLoads</key><true/></dict>

Quick Start

import 'package:clotpulse_flutter/clotpulse_flutter.dart';

final cp = ClotPulse(
  host:  'https://api.clotpulse.app',   // your ClotPulse server
  appId: 'app_xxxxxxxxxxxx',
  key:   'pk_live_xxxxxxxxxxxx',       // Public Key — safe in client apps
);

await cp.connect();

// Get a channel and subscribe
final ch = cp.channels.get('chat:1');

// All events on this channel:
ch.subscribe().listen((msg) => print('${msg.name}: ${msg.data}'));

// Specific event only:
ch.subscribe('message').listen((msg) => print(msg.data));

API Reference

ClotPulse — main client

final cp = ClotPulse(
  host:           'https://api.clotpulse.app',  // required
  appId:          'app_xxxxxxxxxxxx',          // required
  key:            'pk_live_xxxxxxxxxxxx',      // required — Public Key
  autoReconnect:  true,                        // default: true
  reconnectDelay: 2000,                        // ms, default: 2000
);

await cp.connect();   // connect (throws ClotPulseConnectionException on failure)
cp.close();           // disconnect + dispose all resources

cp.channels — channel registry

// Get or create a channel
final ch = cp.channels.get('chat:1');

// Unsubscribe and release
cp.channels.release('chat:1');

ClotPulseChannel — subscribe to events

// Subscribe to ALL events on the channel
ch.subscribe().listen((ClotPulseMessage msg) {
  print('event=${msg.name} data=${msg.data}');
});

// Subscribe to a specific event
ch.subscribe('message').listen((msg) => print(msg.data));
ch.subscribe('order.placed').listen((msg) => handleOrder(msg.data));

ClotPulseMessage

Field Type Description
name String Event name (e.g. 'message')
channel String Channel name (e.g. 'chat:1')
data dynamic Event payload (usually Map<String, dynamic>)
eventId String? Server-assigned unique event ID

cp.connection — connection state

// Listen for state changes
cp.connection.on.listen((ConnectionStateChange change) {
  print('${change.previous} → ${change.current}');
  if (change.current == ConnectionState.connected) {
    print('Online!');
  }
});

// Current state
print(cp.connection.state);       // ConnectionState.connected
print(cp.connection.isConnected); // true / false

States: initialized, connecting, connected, disconnected, failed

ClotPulsePublisher — server-side publish

⚠️ Only use in admin/trusted apps. Never ship the Secret Key in a public app.

final publisher = ClotPulsePublisher(
  appId:     'app_xxxxxxxxxxxx',
  secretKey: 'sk_live_xxxxxxxxxxxx',  // Secret Key
);

// Single event
await publisher.publish('chat:1', 'message', {'text': 'Hello!'});

// Batch
await publisher.publishBatch([
  {'channel': 'chat:1', 'event': 'message',      'data': {'text': 'Hi'}},
  {'channel': 'alerts', 'event': 'order.placed', 'data': {'id': 42}},
]);

Flutter Widget Example

class ChatWidget extends StatefulWidget {
  final String channelName;
  const ChatWidget({super.key, required this.channelName});

  @override
  State<ChatWidget> createState() => _ChatWidgetState();
}

class _ChatWidgetState extends State<ChatWidget> {
  late final ClotPulse _cp;
  final _messages = <ClotPulseMessage>[];

  @override
  void initState() {
    super.initState();
    _connect();
  }

  Future<void> _connect() async {
    _cp = ClotPulse(
      host:  'https://api.clotpulse.app',
      appId: const String.fromEnvironment('CP_APP_ID'),
      key:   const String.fromEnvironment('CP_API_KEY'),
    );

    await _cp.connect();

    _cp.channels.get(widget.channelName)
      .subscribe('message')
      .listen((msg) {
        if (mounted) setState(() => _messages.add(msg));
      });
  }

  @override
  void dispose() {
    _cp.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _messages.length,
      itemBuilder: (_, i) => ListTile(
        title: Text(_messages[i].data['text'] ?? ''),
      ),
    );
  }
}

Secure Token Pattern

For public apps, never hardcode keys. Fetch from your server:

Future<ClotPulse> createClient() async {
  final res = await http.post(
    Uri.parse('https://yourapp.com/api/realtime-token'),
    headers: {'Authorization': 'Bearer $userJwt'},
  );
  final data = jsonDecode(res.body);

  return ClotPulse(
    host:  'https://${data['host']}',
    appId: data['appId'],
    key:   data['apiKey'],  // field name is apiKey, not key
  );
}

Environment Config (no hardcoded keys)

flutter run \
  --dart-define=CP_APP_ID=app_xxxxxxxxxxxx \
  --dart-define=CP_API_KEY=pk_live_xxxxxxxxxxxx

# In code:
const appId = String.fromEnvironment('CP_APP_ID');
const apiKey = String.fromEnvironment('CP_API_KEY');

FCM Push Notifications

ClotPulse can send FCM push notifications to your users when events arrive while they are offline or in the background. Each app configures its own Firebase project from the developer dashboard.

Authentication uses your app's public key — safe to embed in mobile apps.

Setup

1. Add Firebase to your Flutter app (FlutterFire guide)

2. Register a background message handler before runApp:

@pragma('vm:entry-point')
Future<void> _bgHandler(RemoteMessage msg) async {
  // handle background/terminated notifications
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  FirebaseMessaging.onBackgroundMessage(_bgHandler);
  runApp(const MyApp());
}

3. Initialize after login:

final fcm = ClotPulseFcmManager(
  apiUrl:    'https://api.clotpulse.app',
  appId:     'app_xxxxxxxxxxxx',      // your ClotPulse App ID
  publicKey: 'pk_live_xxxxxxxxxxxx',  // your ClotPulse Public Key (safe for client)
  userId:    currentUser.id,          // YOUR system's user ID (opaque to ClotPulse)
  onMessage: (msg) {
    // Notification arrived while app is in foreground
    showInAppBanner(msg.notification?.title);
  },
  onTap: (msg) {
    // User tapped a notification — navigate to relevant screen
    final channel = msg.data['channel'];
    navigateToChat(channel);
  },
);

await fcm.initialize(
  platform: Platform.isAndroid ? 'android' : 'ios',
  channels: ['chat:room1'],  // channels to receive notifications for
);

4. Update channel subscriptions when the user joins/leaves rooms:

await fcm.updateChannels(['chat:room1', 'chat:room2']);

5. Unregister on logout:

await fcm.unregister();

ClotPulseFcmManager API

Parameter Type Description
apiUrl String ClotPulse API base URL
appId String Your ClotPulse App ID (app_...)
publicKey String Your ClotPulse Public Key (pk_live_...)
userId String Your system's user ID (opaque to ClotPulse)
onMessage FcmMessageHandler? Foreground notification handler
onTap FcmNotificationTapHandler? Notification tap handler
requestProvisional bool iOS provisional auth (default: false)

Active Chat Suppression

When a user is actively viewing a chat screen, ClotPulse suppresses FCM notifications for that channel to avoid duplicate alerts. Use ClotPulseActiveChatTracker to manage this automatically with heartbeats.

final tracker = ClotPulseActiveChatTracker(
  apiUrl:    'https://api.clotpulse.app',
  appId:     'app_xxxxxxxxxxxx',
  publicKey: 'pk_live_xxxxxxxxxxxx',
  userId:    currentUser.id,
);

// When entering a chat screen:
await tracker.enterChat('chat:room1');

// When leaving (e.g. in dispose()):
await tracker.leaveChat();
tracker.dispose();  // cancel heartbeat timer

ClotPulseActiveChatTracker automatically sends a heartbeat every 2 minutes to keep the backend TTL alive. If the app is killed, the TTL expires naturally within 5 minutes and notifications resume.


Active Chat via Socket (alternative)

If you already have a ClotPulse socket connection, you can signal active chat over the socket instead:

final cp = ClotPulse(
  host:   'https://api.clotpulse.app',
  appId:  'app_xxxxxxxxxxxx',
  key:    'pk_live_xxxxxxxxxxxx',
  userId: currentUser.id,  // required for active-chat socket events
);

await cp.connect();

// When entering a chat screen:
cp.setActiveChat('chat:room1');

// Every ~2 minutes while on-screen:
cp.activeChatHeartbeat();

// When leaving:
cp.clearActiveChat();

Prefer ClotPulseActiveChatTracker — it handles heartbeats automatically and works independently of the socket connection.


Common Mistakes

❌ Wrong ✅ Correct
host: 'api.clotpulse.app' host: 'https://api.clotpulse.app'
Using secretKey in a public app Use key (Public Key) client-side
key: data['key'] (null) key: data['apiKey']
Not calling cp.close() in dispose() Always call cp.close() to prevent leaks
Forgetting await cp.connect() Await connect before subscribing
Passing a JWT as userId Pass your own user ID (e.g. MongoDB ObjectId, UUID)
Omitting tracker.dispose() Always dispose to cancel the heartbeat timer

Migrating from 1.x

// Before (1.x)
ClotPulse(host: ..., appId: ..., key: ..., userToken: jwtToken)
ClotPulseFcmManager(apiUrl: ..., userToken: jwtToken, appId: ..., ...)
ClotPulseActiveChatTracker(apiUrl: ..., userToken: jwtToken)
await tracker.enterChat(appId: 'app_xxx', channel: 'chat:1')

// After (2.x)
ClotPulse(host: ..., appId: ..., key: ..., userId: currentUser.id)
ClotPulseFcmManager(apiUrl: ..., appId: ..., publicKey: 'pk_live_...', userId: currentUser.id, ...)
ClotPulseActiveChatTracker(apiUrl: ..., appId: ..., publicKey: 'pk_live_...', userId: currentUser.id)
await tracker.enterChat('chat:1')

License

MIT — ClotPulse

Libraries

clotpulse_flutter
ClotPulse Flutter SDK