Abrova
Coming soon. This guide uses Abrova Chat, Abrova Push, which are coming soon. Steps that use Trace work today.

Build a support chat

At the end: a room per customer, offline push fallback and agent replies from your server.
Abrova ChatAbrova PushAbrova TraceFlutter · iOS · Node.jsAdvanced · 45 min

Before you start

Customer roomAgent reply (server)Offline push

A support chat is two integrations stitched together: the live chat (Abrova Chat) and a fallback for when the customer's app is closed (Abrova Push). Without the fallback the customer misses the reply, sends a frustrated follow-up, and your agent answers a conversation that has moved on. Abrova Trace catches the cases where sending fails quietly.

All three products use the same Abrova project: one public key (ab_live_…) for every SDK on every platform, and a server secret (ab_srv_…) for your backend, both in Abrova Console under Project settings → Keys.

Coming soon. Abrova Chat and Push are coming soon. Their code below is the planned API from the Chat and Push docs. The Trace calls work today.

How the pieces fit together#

  1. 1Customer has the chat openMessages arrive live over Chat’s realtime connection.
  2. 2Customer is awayThe app is in the background, closed or on another screen. Chat sends the message to them as a notification through Abrova Push, with the room id in the data.
  3. 3Customer taps the notificationYour tap handler opens the conversation from the room id.
  4. 4Something failsTrace reports the error with the room id, so you can find the customer’s conversation.

Initialize Trace and Push at app start#

Start Trace and Push once in your app entry point, with the same key for both. The Chat client belongs to one signed-in user (it takes the user id), so you create it in step 3. Packages: abrova_trace_flutter_sdk (from Abrova Center today), abrova_push, abrova_chat and abrova_chat_ui (when they launch).

dart
import 'package:flutter/material.dart';
import 'package:abrova_trace_flutter_sdk/abrova_trace_flutter_sdk.dart';
import 'package:abrova_push/abrova_push.dart';

const abrovaApiKey = 'ab_live_your_api_key'; // one key for every SDK

void main() async {
  await AbrovaTrace.initWithZone(
    AbrovaTraceConfig(apiKey: abrovaApiKey, release: '1.0.0'),
    () async {
      WidgetsFlutterBinding.ensureInitialized();
      await AbrovaPush.init(AbrovaPushConfig(apiKey: abrovaApiKey));
      runApp(const MyApp());
    },
  );
}

On iOS:

swift
import AbrovaTrace
import AbrovaPush

AbrovaTrace.shared.initialize(config: AbrovaTraceConfigBuilder(apiKey: apiKey).build())
AbrovaPush.shared.initialize(config: AbrovaPushConfig(apiKey: apiKey))

Create the support room on your server#

Use one room per customer with a fixed external id, such as support-{userId}, so the history stays across sessions and devices. Create it from your backend with the server secret. findOrCreate returns the existing room when you call it again, so you can call it every time the customer opens "Contact support". Package: @abrova/chat.

typescript
import { AbrovaChat } from '@abrova/chat';

const abrovaChat = new AbrovaChat({
  apiKey: process.env.ABROVA_API_KEY!,
  serverSecret: process.env.ABROVA_SERVER_SECRET!, // server only
});

// Your endpoint, behind your own login check: POST /support/room
app.post('/support/room', requireLogin, async (req, res) => {
  const userId = req.user.id;
  const { room } = await abrovaChat.rooms.findOrCreate({
    externalId: `support-${userId}`,
    type: 'group',
    name: 'Support',
    participants: [
      { externalUserId: userId, displayName: req.user.name, locale: 'fa' },
      { externalUserId: 'support-agent', displayName: 'Support', locale: 'fa' },
    ],
    metadata: { type: 'support' },
  });
  res.json({ roomId: room.id });
});

Creating rooms and adding participants take no acting user, so do it only on your server and turn on Server-only room management under Chat security in your project settings. See Authentication. The participant locale picks the language of the push template.

Identify the customer and connect#

In the app, give Trace and Push the user id, create the Chat client and ask your endpoint for the room:

dart
import 'package:abrova_chat/abrova_chat.dart';

final userId = currentUser.id; // the same id your server uses

AbrovaTrace.setUserId(userId);
await AbrovaPush.register(userId: userId); // Chat's offline pushes are sent to this user id

final chat = AbrovaChatClient(AbrovaChatConfig(
  apiKey: abrovaApiKey,
  userId: userId,
  tokenProvider: fetchAbrovaToken, // your code: returns the user token your server mints
));
await chat.connect();

final roomId = await yourApi.openSupportRoom(); // calls POST /support/room from step 2
AbrovaTrace.setExtra('chatRoomId', roomId);

tokenProvider proves who the user is: it returns an Abrova user token that your backend mints with the server secret (POST https://auth.abrova.ir/users/token), and the SDK asks again before the token expires. With Abrova Auth, pass AbrovaAuth.instance.chatTokenProvider and the Auth user id instead.

Render the chat#

If a standard message list and composer fit, use the prebuilt widgets from abrova_chat_ui. AbrovaChatScope creates and connects its own client from the config, so use it instead of the client from step 3, not next to it (read it with AbrovaChatScope.of(context) when you need it):

dart
import 'package:abrova_chat_ui/abrova_chat_ui.dart';

AbrovaChatScope(
  config: AbrovaChatConfig(
    apiKey: abrovaApiKey,
    userId: userId,
    tokenProvider: fetchAbrovaToken,
  ),
  child: MaterialApp(
    home: ChatScreen(roomId: roomId, currentUserId: userId),
  ),
)

For your own UI, subscribe to the room with the step 3 client and listen to its messages:

dart
await chat.subscribeRoom(roomId);

final sub = chat.onMessage.listen((message) {
  if (message.roomId != roomId) return;
  appendMessageToYourList(message); // message.id, message.content, message.senderUserId
});

// Send a message
await chat.sendMessage(roomId, content: composerController.text);

// When the screen closes:
await sub.cancel();
await chat.unsubscribeRoom(roomId);

Turn on the offline push fallback#

You don't send this push yourself. When a message is sent to a room, Chat checks which participants are in the room right now and sends a push through Abrova Push to the others, addressed to their user id. That is why the app registered with AbrovaPush.register(userId: …) in step 3.

Set it up in Abrova Console (Push setup):

  1. In Abrova Push → Templates, create a template. Use the built-in variables, for example {{senderName}} in the title and {{messagePreview}} in the body.
  2. In Abrova Chat, open Notification Settings and link the template, as the default or for the new_message event.

The push carries the room ids (roomId, externalRoomId) and the messageId. Your tap handler uses them to bring the customer straight back to the conversation:

dart
// Taps while the app is running (foreground or background).
AbrovaPush.onNotificationTapped((message) {
  final roomId = message.data?['roomId'];
  if (roomId != null) navigatorKey.currentState?.pushNamed('/support/$roomId');
});

// A tap that launched the app arrives here instead, once your routing is ready.
final initial = await AbrovaPush.getInitialMessage();
final initialRoomId = initial?.data?['roomId'];
if (initialRoomId != null) navigatorKey.currentState?.pushNamed('/support/$initialRoomId');

The agent's replies start the same push whether they come from an app or from your backend:

typescript
await abrovaChat.messages.send(roomId, {
  senderUserId: 'support-agent',
  content: 'Thanks, we are looking into it.',
});

Users can turn chat pushes off, get them only when mentioned, or mute a room; see Notification settings.

Capture chat errors with Trace#

This step works today. Chat can fail in ways the customer only describes as "it stopped working": an expired token, a dropped connection, a rate limit. Wrap each sendMessage call and let Trace report the failure with the room id:

dart
try {
  await chat.sendMessage(roomId, content: text);
} catch (err, stack) {
  await AbrovaTrace.captureException(
    err,
    stackTrace: stack,
    extra: {
      'chatRoomId': roomId,
      'chatAction': 'sendMessage',
    },
  );
  showRetryBanner('Message not sent. Tap to try again.'); // your code
}

Put the room id in extra (or setExtra, as in step 3), not in tags. To check it worked, open Abrova Console → Trace → Issues and open the error: the Context card shows chatRoomId and the user_id from step 3 under Extra data.

Mark conversations resolved#

Chat has no archived state for rooms. When the agent closes the ticket, post a system message so the conversation shows where it ended, and keep the room so its history stays readable:

typescript
await abrovaChat.messages.send(roomId, {
  senderUserId: 'support-agent',
  content: 'This conversation was marked as resolved.',
  type: 'system',
});

// Later, from your backend:
const { messages, hasMore } = await abrovaChat.messages.getHistory(roomId, {
  userId: 'support-agent',
  limit: 50,
});

abrovaChat.rooms.delete(roomId) exists too, but it deletes the room and all its messages for good. Keep your own ticket state (open, resolved) in your backend and show past conversations from it.

Going further#

  • Show "Support is typing" and read receipts with the UI widgets.
  • Keep the "Support replied" text in a push template, so your team can change it without a release.
  • Add breadcrumbs for "opened support" and "sent message", so a chat error shows what the customer did before it.
Pitfalls
  • Agent replies need the server secret — send them from the server.

Where next: Abrova Chat