Presence
Track online status and typing indicators using MQTT retained messages and will messages.
Presence tracking lets your application show who is online, when they were last active, and whether they are currently typing. MQTT's retained messages and will messages make this reliable without any server-side polling or heartbeat infrastructure.
Architecture
Each user publishes a retained message to their presence topic on connect and sets a will message that the broker publishes automatically on disconnect.
Client connects:
→ Publishes retained "online" to presence/{user_id}
→ Sets will message "offline" on presence/{user_id}
Client disconnects (graceful or unexpected):
→ Broker publishes will message "offline" to presence/{user_id}
Other clients:
→ Subscribe to presence/+ to receive all presence updates
→ On subscribe, receive last retained status for each userTopic Design
| Topic | Purpose | Retained |
|---|---|---|
presence/{user_id} | Online/offline status | Yes |
presence/{user_id}/typing | Typing indicator | No |
presence/{user_id}/activity | Last active timestamp | Yes |
The main presence topic uses retained messages so that new subscribers immediately receive the current status of all users - no need to wait for the next status change.
Typing indicators are not retained because they are transient. A typing message is only relevant to clients connected at that moment.
How It Works
Online/Offline Tracking
- Client connects with a will message configured for its presence topic
- Client publishes a retained
onlinepayload topresence/{user_id} - Other clients subscribing to
presence/+receive the retained message immediately - When the client disconnects - whether gracefully or due to network loss - the broker publishes the will message with an
offlinepayload
The will message is the key mechanism. Even if the client crashes or loses network without a clean disconnect, the broker detects the broken connection and publishes the will message automatically.
Will messages are published by the broker, not the client. This means offline detection works even when the client cannot send a goodbye message - the broker handles it.
Typing Indicators
Typing indicators follow a simpler pattern. The client publishes a short-lived message when the user starts typing and stops publishing when they stop.
Implementation
Connect with Will Message
import { connect } from '@cloudsignal/mqtt-client'
const userId = 'user-alice'
const client = await connect({
host: 'wss://connect.cloudsignal.app:18885/',
username: 'alice',
password: 'your-token',
will: {
topic: `presence/${userId}`,
payload: JSON.stringify({
status: 'offline',
timestamp: Date.now()
}),
qos: 1,
retain: true
}
})
// Publish online status (retained)
client.publish(`presence/${userId}`, JSON.stringify({
status: 'online',
timestamp: Date.now()
}), { qos: 1, retain: true })Subscribe to Presence Updates
// Subscribe to all user presence topics
await client.subscribe('presence/+', { qos: 1 })
client.on('message', (topic, payload) => {
const segments = topic.split('/')
// Presence status update
if (segments.length === 2) {
const userId = segments[1]
const data = JSON.parse(payload.toString())
updateUserStatus(userId, data.status, data.timestamp)
}
// Typing indicator
if (segments.length === 3 && segments[2] === 'typing') {
const userId = segments[1]
const data = JSON.parse(payload.toString())
updateTypingIndicator(userId, data.isTyping)
}
})Send Typing Indicators
let typingTimeout = null
function onUserTyping() {
client.publish(`presence/${userId}/typing`, JSON.stringify({
isTyping: true
}))
// Clear previous timeout
clearTimeout(typingTimeout)
// Auto-clear typing after 3 seconds of inactivity
typingTimeout = setTimeout(() => {
client.publish(`presence/${userId}/typing`, JSON.stringify({
isTyping: false
}))
}, 3000)
}Query Presence via REST API
You can also query current presence state from your server using the CloudSignal REST API:
curl -X GET "https://api.cloudsignal.com/v2/topics/presence" \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json"This returns the last retained message for each presence topic, giving you a snapshot of all users' current status.
Graceful Disconnect
For a clean shutdown, publish the offline status explicitly before disconnecting:
async function disconnect() {
await client.publish(`presence/${userId}`, JSON.stringify({
status: 'offline',
timestamp: Date.now()
}), { qos: 1, retain: true })
await client.end()
}This ensures the status updates immediately rather than waiting for the broker's keepalive timeout to detect the disconnect.
Next Steps
- Real-Time Sync - combine presence with collaborative editing
- Offline & Retain - understand how persistent sessions interact with presence
- QoS Levels - choosing QoS for presence messages