Presence & Notifications with the Dart SDK
Presence queries
The client.presence facade wraps the presence service with typed responses.
Room presence
final presence = await client.presence.room(roomId);
// presence.roomId
// presence.participantCount
// presence.participants -> List<PresenceParticipant>
// (userId, displayName, joinedAt, speaking)
User presence
final user = await client.presence.user(userId);
// user.online -> bool
// user.currentRoomId -> String? (null when not in a room)
// user.lastSeen -> ISO-8601 timestamp
Tenant summary
final summary = await client.presence.tenantSummary();
// summary.onlineUsers, summary.activeRooms, summary.totalParticipants
The tenant is derived from your Bearer token — no tenant ID parameter is needed.
Live presence: prefer events over polling
Inside a room, do not poll the presence API. RoomHandle.events pushes RosterSnapshot messages (including per-participant speaking state) and incremental ParticipantJoined/ParticipantLeft events in real time (see Signaling). Use the REST presence facade for lobby screens, dashboards, and "is this user online?" checks outside a room.
Notifications
Webhook subscriptions (server-side event delivery)
The notification service delivers platform events to your backend via webhooks. Webhook management is typically an admin/backend concern — most apps create subscriptions once during setup rather than from the Flutter client. There is no dedicated facade; if you do need to manage subscriptions from Dart, use the authenticated raw client:
final sub = await client.httpClient.post('/webhooks', body: {
'url': 'https://api.myapp.example.com/hooks/ethioconnect',
'secret': 'a-random-32-char-signing-secret!',
'event_types': ['room.created', 'room.closed'],
});
final subs = await client.httpClient.get('/webhooks');
Mobile push (FCM/APNs)
The current notification contract covers webhook subscriptions only — there is no platform endpoint for registering FCM/APNs device tokens, and consequently no SDK support for it (ADR 0022 carries this follow-up forward). Mobile push is therefore composed in your own backend: subscribe your backend to platform webhooks, and fan out pushes to devices yourself (e.g. with firebase_messaging in Flutter and FCM on the server).
The recommended pattern:
- Flutter app obtains an FCM token with
firebase_messagingand registers it with your backend, tied to the platformuser_idyou mint tokens for. - Your backend receives webhook deliveries (e.g.
room.created), verifies the signature with your subscriptionsecret, and looks up the affected users' device tokens. - Your backend sends the push via FCM/APNs; the app deep-links into the room and calls
client.joinRoom(roomId).
import 'package:firebase_messaging/firebase_messaging.dart';
final fcmToken = await FirebaseMessaging.instance.getToken();
// Register with YOUR backend — not with the platform API.
await myBackend.registerDevice(userId: userId, fcmToken: fcmToken!);
Under the hood
The facade maps onto the presence and notification contracts (all Bearer-authenticated):
| SDK call | Endpoint | Response shape |
|---|---|---|
presence.room(roomId) | GET /v1/presence/rooms/{room_id} | { "room_id", "participant_count", "participants": [ { "user_id", "display_name", "joined_at", "speaking" } ] } |
presence.user(userId) | GET /v1/presence/users/{user_id} | { "user_id", "online", "current_room_id" | null, "last_seen" } |
presence.tenantSummary() | GET /v1/presence/tenant/summary | { "tenant_id", "online_users", "active_rooms", "total_participants" } |
| (raw client) | POST /v1/webhooks | Body requires url (HTTPS), secret (min 16 chars, signs deliveries), event_types (e.g. room.created, room.closed). Returns a Subscription (id, tenant_id, url, event_types, active, created_at). |
| (raw client) | GET /v1/webhooks | Subscriptions for the authenticated tenant. |