clotpulse_flutter
Official Flutter SDK for ClotPulse — self-hosted realtime messaging platform.
Works on Android, iOS, and Flutter Web.
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.clotvpn.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.clotvpn.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.clotvpn.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.clotvpn.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.clotvpn.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.clotvpn.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.clotvpn.app' |
host: 'https://api.clotvpn.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