Multi-device sessions
Your users move between devices and the conversation follows them. AI Transport puts every device on the same channel, so the second tab and the phone see the same session in real time.
A multi-device session works because the session is backed by a shared Ably channel, not a single client-to-server HTTP connection. Any device that subscribes to the channel sees every message: user prompts, agent responses, and control signals. Open a second tab, switch to a phone, or share a session with a colleague.
Fan-out to multiple connected devices is automatic. There's no special configuration:
1
2
3
4
5
6
7
8
9
10
11
12
// Client A (laptop): wrap with a TransportProvider for chatId.
<TransportProvider channelName={chatId} codec={UIMessageCodec}>
<Chat />
</TransportProvider>
// Client B (phone): same channel name in its own TransportProvider, different device.
<TransportProvider channelName={chatId} codec={UIMessageCodec}>
<Chat />
</TransportProvider>
// Inside Chat, read the transport from context.
const { transport } = useClientTransport();How it works
Every client connected to the same Ably channel shares the same durable session. When any participant publishes (a user message, an agent response, a cancel signal), every other participant receives it through their channel subscription.
The client transport distinguishes between own turns (started by this client) and observer turns (started by someone else). Both types are tracked, decoded, and added to the conversation tree. The UI updates for every client, regardless of who initiated the action.
Distinguish own and observer turns
The transport separates turns initiated by the current client from those by other participants:
| Type | Origin | Handle |
|---|---|---|
| Own turn | This client sent the HTTP POST that created the turn. | An ActiveTurn with a stream and a cancel() method. |
| Observer turn | Another client or agent created the turn. | Lifecycle events and streamed messages only; no direct stream handle. |
Both types appear in the conversation tree and the UI. The difference is internal routing: own turns have a dedicated stream, observer turns accumulate from channel messages.
Track active turns across clients
useActiveTurns returns a Map<clientId, Set<turnId>> of currently streaming turns:
1
2
3
4
const activeTurns = useActiveTurns();
const isAnyoneStreaming = activeTurns.size > 0;
const isAgentWorking = activeTurns.has('agent-1');This is consistent across every connected client. If client A starts a turn, client B's useActiveTurns updates immediately.
Sync with useChat
When using Vercel's useChat, the useMessageSync hook pushes messages from other clients into useChat's state:
1
2
3
const { chatTransport } = useChatTransport();
const { messages, setMessages } = useChat({ transport: chatTransport });
useMessageSync({ setMessages });Without useMessageSync, useChat only renders messages from its own sends. The sync hook bridges the gap by feeding observer messages into state.
Handle late joiners
A client that connects after the conversation has started loads the full history from the channel:
1
const { nodes, hasOlder, loadOlder } = useView({ limit: 30 });useView loads history on mount. If a response is currently streaming, the late joiner sees it in progress; the lifecycle tracker synthesises missing events so the stream renders correctly.
Identify the client
Each client has a clientId that identifies it across the session. Set the client ID through Ably token authentication so it is verified and cannot be spoofed:
1
2
3
4
5
// In your token endpoint
const token = jwt.sign({
'x-ably-clientId': 'user-123',
// ...
}, keySecret);The clientId is used throughout: turn ownership, cancel scoping ({ own: true } filters by the sender's client ID), and active turn tracking. See authentication for the full setup.
Edge cases and unhappy paths
- Two clients sharing the same
clientIdare indistinguishable to the transport. Cancel signals scoped to{ own: true }cancel turns from both. Use a uniqueclientIdper device when ownership matters. - A late joiner without channel history capability sees the live stream but not the conversation that came before. Capability scoping is part of authentication.
- A client that loses connectivity mid-stream resumes its own view on reconnect. Other clients' views are unaffected.
- Two devices sending messages at the same time create two separate turns on the same session. See concurrent turns for the multiplexing model.
- A regenerate triggered on one device updates the conversation tree on every device. The visible branch on each device depends on its current view selection.
FAQ
Do I need to write any sync code?
No. The channel subscription is the sync. For Vercel useChat, add useMessageSync to bridge observer messages into its state.
How many clients connect to one session?
There is no fixed limit. The channel is the share point; usage scales with the number of subscribers. See the platform pricing page for the connection and message rate limits in effect.
Can two users have different branch selections on the same session?
Yes. Each view holds its own selection. The conversation tree is shared; the view is per-participant. See conversation branching.
What stops a stranger from joining my session?
Channel capabilities. Issue tokens that scope subscribe and publish to the specific channel name for authenticated users. See authentication for scoping examples.
Does presence work across devices?
Yes. Each device enters presence with its own clientId. See agent presence for the patterns.
Related features
- Reconnection and recovery: each device reconnects independently.
- Cancellation: cancel from any device.
- History and replay: late joiners load the full conversation.