Live chat & realtime
How live-chat messages are delivered today (polling), how org realtime SSE fits in, and the path to push-based updates.
At a glance
| Mechanism | Purpose | Who uses it |
|---|---|---|
| Live-chat polling | Fetch new messages for an active ticket | Widget, Ask Humaner, Human Desk, landing |
| Org realtime SSE | Org-wide change signals (ticket updated, inbox synced) | Logged-in dashboard users only |
| Presence heartbeats | Show who is viewing a ticket or thread | Human Desk, inbox |
Key distinction
Live-chat message text is delivered by polling today. Org realtime does not carry message payloads — it tells the dashboard shell that something changed so pages can refresh.
Current architecture: polling
All agent surfaces share the useLiveChatPoll hook from @humaner/shared/use-live-chat-poll. It calls GET /api/v1/live-chat/messages every five seconds (by default) and invokes onUpdate when new messages arrive.
When polling runs
- enabled is true for that surface (see table below).
- A ticketId is set — live chat handoff is active.
- The ticket is not resolved, closed, or timed out.
- The browser tab is visible (hidden tabs pause polling).
Per-surface rules
| Surface | Polls when |
|---|---|
| Widget | Widget open, not preview mode, active ticket |
| Ask Humaner (dashboard) | Ask Humaner dock is open |
| Human Desk | Ticket detail open, live chat supported, not resolved |
| Landing Ask Humaner | Chat panel is open |
Efficiency
Closing Ask Humaner, minimizing the widget, or switching tabs stops live-chat polling. Multiple components watching the same ticket and session share one network request.
Live-chat messages API
GET /api/v1/live-chat/messages authenticates callers differently depending on whether they are visitors or dashboard teammates.
| Caller | Auth | Query params |
|---|---|---|
| Visitor (widget, Ask Humaner) | sessionId + public agent auth | ticketId, sessionId, optional after |
| Dashboard (Human Desk) | NextAuth session, same org as ticket | ticketId, optional after |
The after cursor is an ISO timestamp. Only messages created after that time are returned. After POST /api/v1/live-chat/send, use acknowledgeLiveChatPollMessage() so the next poll does not duplicate the sent line.
Org realtime SSE (dashboard)
The dashboard opens a long-lived Server-Sent Events connection to /api/dashboard/realtime. The server reads org events from Redis every ~1.5s and streams them to the client. Connections reconnect automatically after ~55 seconds.
Event types today
- ticket.updated
- thread.updated
- agent.changed
- inbox.synced
- presence.changed
Events are small signals (type, resourceId, actor, timestamp). There is no message body and no ticket.message event. live-chat/send does not publish org events today.
useOrgRealtime reacts to ticket.updated and similar events by calling router.refresh() — it re-fetches server components. It does not append lines to the live-chat transcript in Human Desk or Ask Humaner.
Visitor surfaces
Widget visitors and landing Ask Humaner cannot use org realtime — they have no dashboard session. They rely on polling (or a future visitor-scoped SSE endpoint).
Polling vs SSE
Hover to zoom
| Polling | Org realtime SSE | |
|---|---|---|
| Latency for new messages | Up to ~5s | N/A for messages today |
| Works for widget / landing | Yes | No |
| Carries message text | Yes | No |
| Requires Redis | No | Yes (for event buffer) |
| Idle cost while chat open | Periodic DB reads | One connection, events on change |
Recommended evolution
- 1
Phase 1 — Human Desk
Publish ticket.message (or similar) from live-chat/send into org realtime. Human Desk listens and appends messages. Keep slow fallback polling on reconnect.
- 2
Phase 2 — Visitor surfaces
Add GET /api/v1/live-chat/stream with the same auth as messages. Wire widget, Ask Humaner, and landing when live chat is active.
- 3
Phase 3 — Hybrid everywhere
SSE for fast delivery when connected; useLiveChatPoll as fallback when SSE drops, Redis is unavailable, or the tab wakes from background.
Keep lifecycle gates
Push delivery does not replace pausing when the UI is closed or the tab is hidden. The enabled and visibility rules in useLiveChatPoll still apply alongside SSE.
Surfaces and transports
Hover to zoom
Operations
Development
Next.js dev compiles routes on demand. First visits to heavy pages (for example Human Desk) can delay all API routes in the same Node process. Pausing idle polls reduces contention but does not remove compile blocking.
Self-hosted
Org realtime requires Upstash Redis — publishOrgEvent no-ops without it. Live-chat polling works without Redis.