Local Real-Time Server
The LocalRealtimeServer is an embedded WebSocket + HTTP server running inside the Electron main process. It provides real-time event delivery to the dashboard renderer without any cloud WebSocket connections.
Architecture
Next.js Renderer (port 3691)
|
| ws://127.0.0.1:3693/ws
v
LocalRealtimeServer (port 3693)
├── WebSocket server (ws library)
│ ├── Channel subscriptions (in-memory Map)
│ ├── Broadcast to subscribers
│ └── Ping/pong keepalive
├── HTTP POST /ingest (cloud event push)
├── HTTP GET /health
└── SQLite event log (better-sqlite3)
├── 24-hour retention
├── UUID deduplication
└── Offline replayHow It Works
1. Server Startup
The server starts automatically when the Electron app launches, following the same singleton pattern as OllamaProxy:
// electron/services/LocalRealtimeServer.ts
const server = getLocalRealtimeServer();
await server.start(); // Binds to 127.0.0.1:36932. Renderer Connection
The dashboard's WebSocket client auto-detects it's running in Electron and connects locally:
// Automatic URL resolution
const wsUrl = window.cortex
? 'ws://127.0.0.1:3693/ws' // Desktop: local
: `wss://api.cortex.acrobi.com/api/ws?token=${jwt}`; // Browser: cloud3. Channel Subscriptions
Same channel model as the cloud WebSocket. The renderer subscribes to specific channels:
{ "type": "subscribe", "channelId": "agent:my-coder-agent" }4. Event Sources
Events flow into the local server from three sources:
| Source | How | Latency |
|---|---|---|
| Local agent activity | Direct injection from Electron main process | ~0ms |
| Cloud webhook push | POST /ingest from cloud's activity broadcaster | Sub-second |
| Polling fallback | EventSyncBridge polls GET /api/events/since | 5-30s |
5. Event Persistence
Every event is stored in the local SQLite database:
CREATE TABLE realtime_events (
id TEXT PRIMARY KEY, -- UUID for dedup
org_id TEXT NOT NULL,
user_id TEXT NOT NULL,
type TEXT NOT NULL, -- e.g., "agent.completed"
payload TEXT NOT NULL, -- JSON
timestamp INTEGER NOT NULL, -- Unix ms (event time)
received_at INTEGER NOT NULL -- Unix ms (local receipt time)
);- Deduplication: Same event from push + poll is stored once (by UUID)
- Retention: Events older than 24 hours are pruned on startup
- Replay: On restart, last 24 hours of events are available for the renderer
Security
- Binds to
127.0.0.1only — not accessible from the network - No authentication required (localhost only)
- Event ingestion (
POST /ingest) validatesorg_idmatches the signed-in user - No sensitive data stored — events are the same payloads that go to the cloud WebSocket
Port Configuration
The port is configurable via the shared config:
// packages/desktop/shared/config.ts
export const LOCAL_REALTIME_PORT = 3693;If port 3693 is in use, the server logs a warning and falls back to the next available port. The renderer discovers the actual port via IPC.
Monitoring
Check server status via IPC or HTTP:
# HTTP health check
curl http://127.0.0.1:3693/health{
"status": "ok",
"connections": 1,
"channels": 3,
"eventsStored": 156,
"uptime": 3600
}Lifecycle
| Event | Behavior |
|---|---|
| App launch | Server starts, loads last 24h events from SQLite |
| Renderer connects | WebSocket upgrade, begin delivering events |
| Cloud push arrives | Store in SQLite, broadcast to subscribers |
| App sleeps/minimizes | Server continues running, events accumulate |
| App closes | Server stops, SQLite persists events for next launch |
| App restarts | Server starts, renderer reconnects, missed events replayed |