Real-time & Presence
Eviworx keeps open surfaces current without anyone having to reload: a list shows a new ticket, a detail picks up a colleague’s status change, and the record names the people who currently have it open. This page describes what of that is visible, the idea behind it, and how it works technically.
What users see
| Surface | Behaviour |
|---|---|
| Detail page of a record | At the top you see the viewers: avatar and name of everyone who has the same record open. When somebody types in a field, that turns into an editing hint — two people about to reply notice it BEFORE it happens. |
| Lists and work queues | A new, changed or deleted row refreshes the list — as do the counters on tabs and pills. Anything the viewer may not see triggers no refresh. |
| Notifications | The bell counts both ways without reloading: a new notification raises the count, and reading or deleting in one window lowers it in all the others too. A decision in the approval inbox likewise updates inbox and task counters in every open window. |
| A foreign change on the open record | The content updates and a short toast names the change. Whoever caused it sees no toast — not even when the system draws a consequence from it at the same moment (an SLA pause, say). |
| Sub-ticket of an open parent ticket | When a sub-ticket is completed or reopened, an open parent ticket follows along: its sub-ticket list, the counters and the SLA display. If a reopened sub-ticket pulls the parent ticket back out of "done", its status moves along too. Details of the relationship: Tickets API. |
| Restricted links | A link that appears only as a placeholder deliberately does not update live: the placeholder reveals nothing about the content, and the live channel must not either. |
| NOC-Wallboard | The wallboard lives entirely off this channel — it is meant for permanent display on a screen. |
The idea
Three decisions shape the behaviour — they also explain why some things deliberately do NOT happen:
- Notify instead of asking. A surface that asks every 30 seconds creates load even when nothing changes — and still lags by up to 30 seconds. Instead the server speaks up when something happened.
- The channel carries no data. A message says "something changed on this object" and which fields are affected — the content itself is fetched through the normal, permission-checked API. That way a real-time channel structurally cannot reveal more than a REST call.
- Your own action is not news. Every message carries its originator. Whoever caused the change gets the content updated but no toast about it — they already know. Other viewers see the hint unchanged.
How it works
The connection is a WebSocket session (Socket.IO) that Traefik forwards to the backend under /socket.io/. It is authenticated with the same session as the REST calls — there is no second login path. Two kinds of channels sit on top of it:
| Channel | Scope | Message content |
|---|---|---|
| Object channel | a single record — joined when somebody opens its detail page | entityType, entityId, event (created, updated, assigned, deleted, restored), the originator and the changed field names |
| List channel | a whole object kind — joined by lists, counters and dashboards | deliberately WITHOUT an object id and without payload: only "something changed in this kind". A user with restricted visibility therefore learns nothing about foreign rows, and the list simply reloads. |
Rights: every channel carries the visibility of its object kind
For each of the 18 object kinds it is declared which permissions open the list channel and which row-level visibility opens the object channel. That row-level visibility is the same one used by the list, the detail page and the global search — including asset type locks, mailbox and group scoping, substitution rules and the grants of a knowledge article.
- When several overviews with different permissions cover the same object kind, each of those permissions opens the list channel — the channel follows the union of those overviews, not a single one of them. User rows therefore also reach a data protection role that may see the erasure backlog without seeing the full user list.
- Without visibility of an object, joining returns a rejection instead of a viewer list.
- A bulk subscription (a list subscribing to many rows at once) silently filters out invisible objects.
- An unknown object kind is rejected, and one subscription covers at most 500 objects — the same ceiling as a list page.
- A viewer’s name and avatar always come from the database, never from the client message.
- Account status beats session state: when an account is locked or archived the server drops its open connections immediately, and a new connection is refused with "Account deactivated" — the same status answers an API call with 403 ACCOUNT_DEACTIVATED. An open tab therefore receives no further live data, not even until the session ends.
- The same holds for the role: when a role is deactivated, every account holding it loses access — open connections are dropped even though the accounts themselves stay active. Otherwise the live channel would keep running while every API call is already being refused.
Viewer state
Whoever has a detail page open is "viewing"; typing in a field makes them "editing"; staying idle makes them "idle". The state lives in Redis with a five-minute lifetime and is refreshed by a periodic heartbeat from the UI. A crashed browser or a closed laptop therefore disappears from the list by itself — no cleanup job needed.
Bundling changes that follow each other closely
One mutation often triggers several consequences — a status change pauses an SLA deadline, a link changes both sides. Messages about the same object within 50 milliseconds are therefore merged into ONE, with the union of the changed fields. The originator is preserved even when the system triggers follow-up changes at the same moment (the SLA pause, say), so nobody gets a toast about their own change.
How much real-time each object kind has
Both channels exist for eighteen object kinds, but not every surface uses both. The viewer display requires a detail page that joins the object channel — lists and counters only need the list channel.
| Scope | Object kinds |
|---|---|
| Viewer display and live detail | TICKET · INCIDENT · PROBLEM · CHANGE · ASSET · CONTRACT · LICENSE · KB |
| Live updates of lists and counters | additionally CHANGE_TEMPLATE · ASSET_TYPE · ELIBRARY · ABSENCE · COST_CENTER · USER · AGENT_GROUP · INVENTORY_SESSION · WORKFLOW · SAVED_VIEW |
In the second group, overviews and counters update themselves, but there are no viewer avatars — the eLibrary, for instance, has no detail surface that could show them. Its channel covers documents, collections and categories under one object kind. Saved views run over these same two channels: the picker, the sidebar and the result counters follow along without polling.
Operations
- Gateway: Traefik forwards /socket.io/ to the backend (WebSocket upgrade). A reverse proxy in front must permit the upgrade as well, otherwise the connection falls back to long polling or fails.
- Redis: holds the viewer state. Without Redis the avatars disappear — the application stays usable, only without the live comfort.
- No substitute for the API: The channel is a hint channel for the UI, not an integration interface. For your own integrations the REST endpoints are the way; they deliver the same data with the same rights.
- Multiple backend instances: Sessions are bound to the instance that accepted them. Scaling the backend horizontally requires sticky sessions at the gateway.
Related pages
- Notifications — the channels of a notification; the in-app path uses this very connection
- Security — the permission checks of the real-time channels in the security context
- Permissions & RBAC — the permissions and visibility rules the channel checks rely on
- Container Architecture — gateway routes and Redis in context