Presence answers two questions: "is this user online right now?" and "what should I tell the routing engine about them?". ChatPresenceService keeps both answers in Redis so any pod can read consistent state without leader election.
Storage model
Three Redis key families back the service:
chat-socket:{socketId}(TTL 120s) — per-connection metadata. One row per live websocket.chat-identity:{orgId}:{role}:{email}— set of socketIds for this identity.SCARD > 0means online.chat-presence:{orgId}:{role}:{email}— aggregated metadata:name,status,skills,activeChats, location.
A user with three open tabs has one identity row and three socket rows. Closing one tab decrements the identity set; closing the last one removes the presence record.
Agent status
Agents have an explicit status independent of "online". The set is available | busy | away.
Set your own status
socket.emit('set-status', { status: 'busy' });
Or via REST for admin override:
/chat/agents/{email}/statusJWT{ "status": "available" }
The server broadcasts a presence-change event to every connected socket so dashboards update instantly.
Read agent presence
/chat/agents/onlineJWTReturns currently online agents. Optional query: status (filter by available, busy, away) and skill (matches presence.skills[]).
curl "https://appengine.appmint.io/chat/agents/online?status=available&skill=billing" \
-H "orgid: my-org" -H "Authorization: Bearer <jwt>"
/chat/agents/{email}/presenceJWTThe full presence record for one agent.
Customer presence
Customers don't set status — they're either connected or not. Online customer presence is tracked the same way agents are; useful for the "live visitors" panel in the admin console.
/chat/customers/onlineJWT/chat/customers/{email}/journeyJWTThe customer journey endpoint joins live presence with the most recent web_visit rows so admins can see both "where they are now" and "where they've been". Optional ?limit=50 controls the depth.
Stats
/chat/presence/statsJWTAggregated counts: agents broken down by status, total online customers. Drives the header counters in the admin UI.
Active sessions
/chat/sessionsJWTEvery live socket in the org. Returns the same shape as the per-socket Redis row (email, role, ipAddress, userAgent, currentUrl, etc.) for live audit views.
Subscribing to changes
Every status mutation emits a presence-change to the whole /chat namespace:
socket.on('presence-change', (evt) => {
// { email, name?, role, status: 'online' | 'offline' | 'available' | 'busy' | 'away',
// timestamp }
});
If you only care about a specific user, key off evt.email. If you only care about agents, key off evt.role === 'agent'.
A user can be online (socket connected) and busy (status flag) at the same time. The status field is meaningful only for agents — customer presence reports online or offline.
TTL safety net
If a pod crashes mid-conversation the per-socket row self-expires in <=120s and the identity set is cleaned by the next read. There is also an hour-long SAFETY_TTL_SECONDS on identity-level keys so absolute orphans get garbage-collected.