Cross-User Sync
When multiple team members use the Cortex desktop app, events from one user's agents need to reach other team members. The EventSyncBridge handles this without persistent cloud WebSocket connections.
How It Works
Team Member A (Desktop) Cloud Team Member B (Desktop)
======================== ===== ========================
Agent completes task ──────> API writes to DB
|
v
broadcastActivity()
|
┌────────┴────────┐
v v
WebSocketRoom DO Device Fanout
(web-only users) |
v
POST callback URLs
|
┌─────────────────┘
v
LocalRealtimeServer
POST /ingest ──> SQLite ──> WebSocket broadcast
|
v
Dashboard UI
"Agent completed!"Device Registration
On startup, the desktop app registers its callback URL with the cloud:
POST /api/v1/devices/register
{
"deviceId": "device-abc123",
"orgId": "org-456",
"callbackUrl": "https://device-abc123.u.acrobi.com/ingest",
"platform": "desktop",
"version": "1.0.0"
}The callback URL comes from the built-in Cloudflare Tunnel, which gives each desktop app a stable public URL without port forwarding.
On shutdown, the app deregisters:
DELETE /api/v1/devices/deregister
{ "deviceId": "device-abc123" }Primary: Webhook Push
When broadcastActivity() fires in the cloud worker, it fans out to registered device callback URLs:
- Cloud reads registered devices for the relevant
orgId - POSTs the event to each device's callback URL
- Fire-and-forget with 2-second timeout per device
- Failed deliveries are not retried (polling fallback covers gaps)
Latency
- With tunnel active: Sub-second (direct HTTP POST)
- Tunnel unavailable: Falls back to polling (see below)
Fallback: Adaptive Polling
If the webhook push can't reach the desktop (NAT, firewall, tunnel down), the EventSyncBridge polls the cloud API:
GET /api/events/since?cursor=1712150400000&orgId=org-456Adaptive Interval
The polling interval adjusts based on activity:
| Condition | Poll Interval |
|---|---|
| Events received in last poll | 5 seconds |
| No events for 1 minute | 10 seconds |
| No events for 5 minutes | 30 seconds |
| Push webhook working | Polling disabled |
Deduplication
Events received via both push and poll are deduplicated by their UUID id field in the local SQLite database. The INSERT OR IGNORE pattern ensures each event is stored exactly once.
Event Envelope
All cross-user events share a common format:
interface CrossUserEvent {
id: string; // UUID for deduplication
orgId: string; // Organization scope
userId: string; // Who triggered the event
type: string; // Event type (agent.completed, task.created, etc.)
payload: object; // Event-specific data
timestamp: number; // Unix ms when event occurred
}Tunnel Management
The desktop app includes a TunnelManager service that establishes a Cloudflare Tunnel for inbound webhook delivery:
| Feature | Detail |
|---|---|
| Protocol | Cloudflare Tunnel (cloudflared) |
| URL format | https://<device-id>.u.acrobi.com |
| Authentication | Device-specific token, rotated on registration |
| Auto-start | Starts when cross-user sync is enabled |
| Fallback | If tunnel fails, switches to polling automatically |
Feature Flag
Cross-user sync via tunnel is controlled by the TUNNEL_ENABLED feature flag in packages/desktop/shared/config.ts. Currently defaults to false — polling is the active fallback.
Privacy & Security
- Device callback URLs are only stored for the lifetime of the registration
- Tunnel traffic is encrypted end-to-end via Cloudflare
- Events contain only the same data that goes through the cloud WebSocket
- No sensitive data (credentials, API keys) flows through the sync channel
- Device registration requires authenticated user token