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

MechanismPurposeWho uses it
Live-chat pollingFetch new messages for an active ticketWidget, Ask Humaner, Human Desk, landing
Org realtime SSEOrg-wide change signals (ticket updated, inbox synced)Logged-in dashboard users only
Presence heartbeatsShow who is viewing a ticket or threadHuman 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

SurfacePolls when
WidgetWidget open, not preview mode, active ticket
Ask Humaner (dashboard)Ask Humaner dock is open
Human DeskTicket detail open, live chat supported, not resolved
Landing Ask HumanerChat 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.

CallerAuthQuery params
Visitor (widget, Ask Humaner)sessionId + public agent authticketId, sessionId, optional after
Dashboard (Human Desk)NextAuth session, same org as ticketticketId, 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

PollingOrg realtime SSE
Latency for new messagesUp to ~5sN/A for messages today
Works for widget / landingYesNo
Carries message textYesNo
Requires RedisNoYes (for event buffer)
Idle cost while chat openPeriodic DB readsOne connection, events on change

Recommended evolution

  1. 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. 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. 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.