Online presence system
Zenoba automatically detects connected users and displays their status in real time. This page documents the technical operation of the presence system.
How it works
The presence system relies on a heartbeat mechanism. Each client sends a periodic signal to the server to indicate activity. The server maintains a list of online users and makes it available to frontend components.
Send heartbeat
The client sends a POST /presence/heartbeat every 30 seconds with the current project_id.
Server receives
The server records user_id, project_id and timestamp in an in-memory store (Python dictionary).
Store update
The in-memory store is periodically cleaned: entries older than 90 seconds are removed.
Query online users
A GET /presence/online?project_id=xxx returns users whose last heartbeat is less than 90 seconds old.
Display avatars
The OnlineAvatars component displays stacked avatars of online users in the top bar.
Backend API
Two REST endpoints manage presence. They are mounted on the /api/v1/presence router.
/api/v1/presence/heartbeatRecords a presence signal for the authenticated user. Call every 30 seconds from the frontend.
// Request body
{
"project_id": "2026-ZEN-001"
}
// Response 200
{
"ok": true
}/api/v1/presence/online?project_id=2026-ZEN-001Returns the list of online users for a given project. Filters users whose last heartbeat is older than 90 seconds.
// Response 200
{
"users": [
{
"user_id": "u-abc123",
"name": "Marie Tremblay",
"avatar": null,
"last_seen": "2026-07-30T14:22:00Z"
}
]
}Frontend Hooks
Two React hooks encapsulate presence logic. They automatically manage intervals and cleanup.
useHeartbeat(projectId)
Automatically sends a heartbeat every 30 seconds. Cleans up on component unmount. Use in the main project layout.
import { useHeartbeat } from '@/lib/use-heartbeat';
function ProjectLayout({ projectId }: { projectId: string }) {
// Envoie un heartbeat toutes les 30s automatiquement
useHeartbeat(projectId);
return <div>...</div>;
}useOnlineUsers(projectId)
Fetches the list of online users every 30 seconds via polling. Returns { users, loading }.
import { useOnlineUsers } from '@/lib/use-online-users';
function TeamStatus({ projectId }: { projectId: string }) {
const { users, loading } = useOnlineUsers(projectId);
return (
<div>
{users.map(u => (
<span key={u.user_id}>{u.name}</span>
))}
</div>
);
}Components
Two React components display presence in the interface.
<OnlineAvatars />
Displays stacked avatars of online users, with a "+N" counter when count exceeds max. Used in the top bar.
projectIdstring-maxnumber5showLabelbooleanfalsesize"sm" | "md" | "lg""md"<OnlineAvatars
projectId="2026-ZEN-001"
max={3}
showLabel
size="sm"
/><OnlineUserList />
Detailed list of online users with name, domain and connection time. Used in the project Team page.
Integration points
The presence system is integrated at three points in the application.
AppShell (main layout)
useHeartbeat() is called in the main layout. As soon as the user is logged in and a project is selected, the heartbeat starts automatically.
Top bar (Topbar)
The OnlineAvatars component is displayed in the topbar on the right. It shows collaborators currently connected to the same project.
Team page
OnlineUserList displays the full list of online users with their detailed status (name, domain, last activity).
Roadmap
Phase 1 — HTTP Polling
AvailableCurrent implementation. POST heartbeat every 30s, GET polling every 30s, in-memory store. Simple, reliable, sufficient up to 100 concurrent users.
Phase 2 — Server-Sent Events (SSE)
Coming soonReplace GET polling with a persistent SSE stream. Server pushes presence updates in real time. Reduces latency from 30s to under 1s. Requires a dedicated SSE endpoint.
Phase 3 — Redis + WebSocket
Coming soonShared Redis store across API instances (horizontal scaling). Bidirectional WebSocket for presence + collaborative cursors + typing indicators. Target architecture for 1000+ concurrent users.