Sincronización
Desktop y móvil, siempre al día. QR pairing, E2EE, relay.
Sincronización — Desktop y móvil, siempre al día
Monolith Desktop es la fuente de verdad. El Companion recibe tus datos por WebSocket cifrado de extremo a extremo. El servidor de relay nunca ve tu información — solo reenvía bytes entre tus dispositivos.
¿Para qué? Trabajar en el móvil (capturar ideas, ver tu agenda, usar el Pulso) sin perder nada. Todo vuelve al Desktop.
Cómo funciona
Desktop (vault .md)
↕
Desktop Electron App
↕ (DaySnapshot / deltas cifrados)
Relay Server (WebSocket)
↕
Companion App (SQLite cache)
- Desktop envía un DaySnapshot con todo tu día: foco, tareas, ecos, inbox, rocas, pulso activo
- Companion recibe y aplica los datos a su caché SQLite
- Companion envía deltas cuando haces cambios: completar subtarea, agregar a inbox, iniciar pulso
- Desktop recibe y aplica los deltas a tu vault
Pairing por QR
- En Desktop: Settings > Sync > “Generar QR”
- En Companion: abre la app y apunta la cámara al QR
- El pairing se establece automáticamente
- Los datos comienzan a sincronizar
¿Qué se intercambia? Una clave de sesión cifrada. El relay solo ve el session_id (un UUID), nunca el contenido.
Seguridad
| Capa | Qué protege |
|---|---|
| NaCl secretbox | Cifrado simétrico de todos los mensajes WebSocket |
| Session key | Derivada del QR, almacenada en SecureStore (Companion) |
| Relay stateless | No persiste mensajes, no guarda datos, no parsea contenido |
| E2EE | El servidor solo reenvía bytes cifrados. Nunca ve texto plano |
El relay
Monolith Relay es un servidor WebSocket que conecta el Desktop con el Companion. Los mensajes viajan cifrados de extremo a extremo — el relay solo reenvía bytes entre pares vinculados. Cero dependencias (puro Bun runtime).
Repositorio de GitHub: monolith-relay
Arquitectura
Desktop ──WS──> Monolith Relay ──WS──> Companion
|
/state HTTP API
|
(Pulse state cache)
- Sesiones: Dos dispositivos (desktop + companion) comparten una sesión identificada por un UUID v4.
- Pairing: El primer dispositivo se une (esperando). El segundo se une (vinculado). Ambos reciben
session_paired. - Reenvío: Los mensajes de un peer se reenvían al otro. Si el peer está offline, los mensajes se encolan (FIFO, máximo 1000).
- State API: El Desktop publica el estado del Pulso vía HTTP POST
/state. El Companion lo lee vía GET/state(para el widget sin necesidad de WebSocket). - Reconexión: Si un dispositivo se reconecta con el mismo
device_id, el socket anterior se reemplaza (código 4014). - Revocación: Un dispositivo puede enviar
revokepara desvincular la sesión permanentemente. - Unlink: Un dispositivo puede enviar
unlinkpara desconectar sin revocar.
Quick Start
git clone https://github.com/OzkrRouj/monolith-relay
cd monolith-relay
bun install
bun run dev # desarrollo con --watch
bun run start # producción
Variables de entorno
| Variable | Default | Descripción |
|---|---|---|
PORT | 3005 | Puerto de escucha del relay |
STATE_AUTH_SECRET | '' | Si se define, HTTP /state requiere header X-Monolith-Secret |
Despliegue (Dokploy)
- Push del repo a GitHub
- En Dokploy, crear nuevo servicio → apuntar al repo
- Variables de entorno:
PORT=3005,STATE_AUTH_SECRET=<tu-secreto> - Exponer puerto 3005 (Traefik maneja SSL)
Protocolo
Join
El cliente envía inmediatamente después de conectar WebSocket:
{ "type": "join", "session_id": "<uuid-v4>", "device_id": "<unique-per-device>", "version": 1 }
Mensajes del relay (servidor → cliente)
| Tipo | Cuándo |
|---|---|
session_paired | Segundo dispositivo se unió, o primero en espera |
peer_connected | Peer (re)conectado |
peer_disconnected | Peer desconectado |
peer_revoked | Sesión revocada por el otro peer |
device_unlinked | Peer envió unlink |
server_shutdown | Servidor cerrándose (graceful) |
Mensajes del cliente
| Tipo | Acción |
|---|---|
join | Identificarse y crear/unirse a sesión |
unlink | Desconexión graceful sin revocar |
revoke | Revocar sesión permanentemente (el otro peer recibe peer_revoked) |
Todos los demás mensajes se reenvían transparentemente al peer.
HTTP Endpoints
| Endpoint | Descripción |
|---|---|
GET /health | Estado del servidor, versión, sesiones activas |
GET /state?sessionId=<uuid> | Estado cacheado del Pulso para una sesión (requiere X-Monolith-Secret si STATE_AUTH_SECRET está definido) |
POST /state | Desktop publica estado del Pulso para una sesión |
Qué datos se sincronizan
| Dirección | Datos |
|---|---|
| Desktop → Companion | DaySnapshot: foco, tareas del día, ecos, inbox, rocas, pulso activo |
| Companion → Desktop | Deltas: subtask_complete, inbox_append, inbox_delete, inbox_update, pulso_start, pulso_end, foco_change, task_activate, roca_changed |
Regla: Desktop siempre tiene prioridad (LWW — Last Writer Wins). Si hay conflicto, gana el Desktop.
Offline
El Companion mantiene una cola de deltas en AsyncStorage. Cuando te reconectas, los deltas pendientes se envían automáticamente. No pierdes nada si estuviste sin conexión.
El Background Fetch ejecuta un ciclo de sync cada 15 minutos incluso cuando la app está cerrada.