Zenoba
Documentation

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.

1

Send heartbeat

The client sends a POST /presence/heartbeat every 30 seconds with the current project_id.

2

Server receives

The server records user_id, project_id and timestamp in an in-memory store (Python dictionary).

3

Store update

The in-memory store is periodically cleaned: entries older than 90 seconds are removed.

4

Query online users

A GET /presence/online?project_id=xxx returns users whose last heartbeat is less than 90 seconds old.

5

Display avatars

The OnlineAvatars component displays stacked avatars of online users in the top bar.

ParameterValue
Heartbeat interval30 seconds
Inactivity threshold90 seconds
StorageIn-memory (Python dict)
CleanupOn every /online request

Backend API

Two REST endpoints manage presence. They are mounted on the /api/v1/presence router.

POST/api/v1/presence/heartbeat

Records 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
}
GET/api/v1/presence/online?project_id=2026-ZEN-001

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

PropTypeDefault
projectIdstring-
maxnumber5
showLabelbooleanfalse
size"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

Available

Current 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 soon

Replace 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 soon

Shared Redis store across API instances (horizontal scaling). Bidirectional WebSocket for presence + collaborative cursors + typing indicators. Target architecture for 1000+ concurrent users.