Skip to main content
← Blog

Syncing Mobile and Desktop in Real Time

VauDium ·

How we chose SSE to keep two clients connected to the same API in sync.

Syncing Mobile and Desktop in Real Time

Fecit started as a mobile app. Then we built a desktop app. Tauri v2 with React and Vite on top, pointing at the same API server as mobile.

Two apps, one problem: when you complete a task on mobile, the desktop still shows it incomplete. And vice versa. Refreshing works, but that’s not sync.

The Options

1. Refresh on window focus

The simplest approach. When the desktop window gains focus, re-fetch from the API. Zero server changes. But it’s not real-time. If both screens are open side by side, one shows stale data until you click it.

2. Polling

Check for changes every 30 seconds. Simple, but wasteful. 29.9 out of every 30 seconds, the answer is “nothing changed.” Server load scales linearly with connected clients.

3. WebSocket

Bidirectional, real-time. But we only need server-to-client notifications. The data updates themselves go through REST. WebSocket requires a different protocol, separate handshake, frame parsing, and potentially proxy reconfiguration. Overkill.

4. SSE (Server-Sent Events)

Server-to-client, unidirectional events over plain HTTP. Browsers have a built-in EventSource API with automatic reconnection. Works with existing HTTP infrastructure.

Why SSE

The deciding factor was direction.

Data changes go through REST API calls. That already works. What we need is a notification: “something changed on another device.” That notification flows one way — server to client. There’s nothing the client needs to send back through this channel.

SSE does exactly this.

WebSocketSSE
DirectionBidirectionalServer to client
Protocolws://Plain HTTP
ReconnectionManualBuilt-in
InfrastructureMay need proxy changesUses existing HTTP
ComplexityHigherLower

The Architecture

We add an /api/achiever/events/stream SSE endpoint to the server. When an authenticated user connects, the server sends events whenever that user’s data changes.

event: task_record_updated
data: {"id": "abc123", "action": "update"}

event: task_record_updated
data: {"id": "def456", "action": "complete"}

The client receives the event and fetches only the changed data. Not a full refresh — a targeted update.

The Mobile Question

Desktop is straightforward. Tauri is web-based, so EventSource works out of the box.

Mobile is trickier.

React Native needs a polyfill for SSE. Libraries like react-native-sse exist. But there’s a more fundamental issue.

Battery and background. When a mobile app goes to the background, the OS can kill network connections. iOS is particularly aggressive about this. Maintaining a persistent connection drains battery.

So we chose a pragmatic combination:

  • Desktop: SSE always connected
  • Mobile: SSE only while in the foreground. In the background, fall back to the existing syncAt-based incremental sync, and catch up when returning to the foreground

Mobile already had a solid incremental sync system based on syncAt watermarks. SSE just adds real-time responsiveness while the app is actively being used. It’s a layer on top, not a replacement.

Not Yet Built

As of this writing, SSE is a plan, not an implementation.

The concept is simple: server publishes events, clients subscribe. But implementation always surfaces surprises. Connection recovery, event ordering, server-side memory management for open connections.

We’ll cover the actual implementation in a follow-up post.