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:
    path: ../sdks/flutter   # local — until published to pub.dev
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');

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

License

MIT — ClotPulse

Libraries

clotpulse_flutter
ClotPulse Flutter SDK