Metiendo Syncthing en el bolsillo: construyendo una app iOS open source con Claude
Uso Syncthing desde hace años. Es una de esas raras piezas de software que hace exactamente una cosa y la hace bien: mantiene carpetas sincronizadas entre tus propios dispositivos, directamente y con cifrado de extremo a extremo, sin cuenta en la nube y sin una empresa sentada en el medio. Mi MacBook, mis servidores y mis backups hablan Syncthing.
Mi iPhone no.
Esta es la historia de cómo lo arreglé en unos dos días, trabajando con Claude, el asistente de código de Anthropic, y por qué el resultado, SuperSynch, es open source bajo la misma licencia que el propio Syncthing.
- Código: https://github.com/santacroce-tech/supersynch
- Sitio: https://santacroce-tech.github.io/supersynch/
Por qué una versión iOS open source importa
Syncthing corre en Linux, Windows, macOS, los BSDs y más. En Android hay apps de la comunidad. En iOS la situación siempre ha sido más difícil. No hay app oficial, porque las reglas de plataforma de Apple hacen incómodo un daemon de sincronización en segundo plano: las apps no pueden correr continuamente, no pueden escanear la red libremente, y viven dentro de un sandbox.
Hay gente buena trabajando en esto. Synctrain, por ejemplo, es una app iOS open source construida sobre Syncthing. Pero cuando miré lo que yo quería, algunas cosas quedaron claras:
- El teléfono debería ser un dispositivo Syncthing de verdad. Nada de control remoto ni servicio de relay mío: el iPhone y el Mac deberían hablar entre sí directamente, de la misma forma en que mis otras máquinas lo hacen.
- Tiene que ser open source. Software que sostiene mis archivos, mis claves de dispositivo y mis conexiones de red es software que quiero poder leer. Y Syncthing es un bien común; cualquier cosa construida sobre él debería devolverle algo a ese bien común.
- Quería entender cada capa. Desde el motor Go hasta las vistas en SwiftUI, quería algo que pudiera mantener, explicar y cambiar.
Así que decidí construir uno, y hacerlo en abierto.
Un comienzo equivocado, y por qué fue útil
Mi primera especificación estaba, francamente, mal. Pedí un “controlador remoto”: una app nativa que habla con instancias de Syncthing corriendo en otro lugar (un NAS, un servidor, un Mac) a través de la REST API de Syncthing.
Claude construyó exactamente eso, y lo construyó bien:
- una app SwiftUI para iPhone y iPad,
- soporte para múltiples servidores,
- claves de API en el Keychain,
- un flujo trust-on-first-use para los certificados HTTPS autofirmados de Syncthing (mostrar el fingerprint, confirmar, fijar),
- un dashboard en vivo movido por el stream de eventos de Syncthing,
- tests que corrían contra una instancia Syncthing real.
Luego miré la cosa y me di cuenta de que derrotaba el propósito entero. Para usarla, habría tenido que exponer la GUI de Syncthing de mi MacBook a mi teléfono. Y aun así no pondría un solo archivo en el teléfono. No quería un control remoto. Quería que el teléfono fuera un nodo.
Así que pivotamos. Lo que me sorprendió fue cuánto sobrevivió al pivot: los modelos, la lógica de procesamiento de eventos, la mayor parte de la UI. Más sobre esto abajo.
Cómo funciona: Syncthing, compilado para iOS
Syncthing está escrito en Go, y Go puede compilarse en un framework iOS usando gomobile. El plan quedó así:
App SwiftUI ──► Cliente Swift (async/await) ──► Puente Go ──► Syncthing v2.1.5
(gomobile) (lib/syncthing)
Antes de escribir cualquier código de puente, Claude fue a leer el código de Syncthing para comprobar si el embedding era realmente posible. Lo era, pero con algunas peculiaridades que ya son una buena historia por sí solas.
La peculiaridad del module path. Las tags de release de Syncthing v2 no se pueden usar con un go get normal. El sistema de módulos de Go espera un sufijo /v2 en la ruta del módulo, y el de Syncthing no está nombrado así. La solución fue fijar el commit exacto de v2.1.5 como pseudo-versión. Cosa pequeña, pero el tipo de detalle que se come una tarde.
Los assets de la web GUI. lib/syncthing arrastra el paquete de la web GUI, cuyos assets son generados por los propios scripts de build de Syncthing. Syncthing tiene una build tag noassets exactamente para este caso, y como el nodo embebido no necesita web GUI, todo se compila con ella.
Los folder summaries faltantes. Este fue sutil. En un test temprano, las carpetas en el nodo embebido nunca actualizaban su conteo de archivos. Después de cavar en el código, encontramos la causa: Syncthing solo arranca su folder summary service, que emite los eventos FolderSummary y FolderCompletion, junto con la web GUI. Sin GUI, sin summaries. El puente ahora arranca ese servicio él mismo.
Alcanzando el modelo. lib/syncthing expone deliberadamente parte de sus internals para quienes lo embeben. Pero algunas cosas que la GUI muestra no están en ese subset: estadísticas de conexión por dispositivo, dispositivos pendientes recordados entre reinicios, estadísticas de carpeta y versiones de archivo. El puente alcanza el modelo completo a través de un pequeño pedazo documentado de reflection. Está verificado por tests y marcado para re-chequear en cada upgrade de Syncthing. Es el compromiso más pragmático del proyecto, y me encantaría que se volviera innecesario (ver abajo).
Manteniendo el lado Swift aburrido
La decisión de diseño con la que estoy más contento: cada pedazo de dato cruza la frontera Go→Swift como JSON con la misma forma exacta que la REST API de Syncthing. El puente no es un servidor REST; nada escucha en la red. Pero sus funciones devuelven las mismas formas que /rest/db/status, /rest/system/connections, /rest/events y así.
Eso significó que el código Swift de la versión de control remoto abandonada pasó casi sin cambios:
- los modelos de decoding (escritos para ser tolerantes, porque la API de Syncthing evoluciona),
- el event reducer puro que convierte el stream de eventos de Syncthing en estado de UI,
- la sesión en vivo que sigue eventos y cae a polling como fallback.
El comienzo equivocado se convirtió en la fundación.
Un detalle más: las llamadas Go bloquean. Un long-poll de eventos puede quedarse esperando hasta su timeout. Así que el wrapper Swift las corre en una cola dedicada en lugar del pool cooperativo de hilos de Swift, y cancelación retorna inmediatamente en vez de esperar a Go.
Qué hace la app
- Pairing: tu iPhone muestra su device ID de Syncthing como código QR; escaneas o pegas el ID de tu Mac.
- Integración con la app Files: las carpetas sincronizadas viven en Files › En Mi iPhone › SuperSynch, usables por cualquier otra app. También hay un browser integrado con Quick Look.
- Sync selectivo: un editor de patrones de ignore te deja mantener solo parte de una carpeta grande en el teléfono.
- Versiones y conflictos: versionado de archivos con restore, y una lista de conflictos donde eliges qué copia gana.
- Dispositivos no confiables: compartir una carpeta cifrada con contraseña.
- Consciente de los datos: sync solo en Wi-Fi, un switch de Modo de Bajos Datos y límites de ancho de banda.
- iPad: layout de tres columnas con atajos de teclado. iPhone y iPad comparten un solo codebase; solo dos archivos pequeños difieren.
- Sin telemetría: reporte de uso y crash de Syncthing, auto-upgrade y web GUI están todos apagados.
Siendo honesto sobre iOS
Insistí en que el README llevara una sección Limitations clara, porque nada erosiona la confianza más rápido que una app que promete de más.
- Sin sync continuo en background. iOS no deja que una app corra todo el tiempo. SuperSynch sincroniza mientras está abierta, termina transferencias por unos 30 segundos después de que la dejas, y usa el tiempo de background que iOS concede, a menudo mientras cargas. En la práctica: abres la app, y se pone al día.
- Sin descubrimiento de red local aún. Syncthing encuentra dispositivos en una LAN con broadcasts UDP, que en iOS necesitan una entitlement de multicast que Apple tiene que aprobar. Hasta entonces, descubrimiento global, relays o una dirección introducida manualmente hacen el trabajo.
- Archivos enteros, no bajo demanda. Todo lo que no esté ignorado se descarga. Los archivos bajo demanda están en el roadmap.
Bugs que me enseñaron algo
Tres momentos de la construcción se destacan.
“No toques mi Syncthing de verdad.” A mitad de camino, mientras probaba en el simulador, Claude notó que el puerto 22000 ya estaba ocupado, por el Syncthing real de mi Mac, que tenía una conexión viva con otro de mis dispositivos. El simulador comparte la red del Mac, así que un test descuidado podría haberse conectado a él. Se detuvo, me dijo, y cambió el setup para que los tests nunca toquen puertos por defecto. En el simulador la app ahora usa su propio puerto. Agradecí que tratara a mi máquina como algo con lo que tener cuidado, no solo un entorno de build.
El crash que no era. En mi iPhone, Xcode se detuvo con EXC_BAD_ACCESS. En vez de adivinar, Claude lanzó la app instalada en el teléfono sin el debugger y mostró en streaming su consola: Syncthing arrancó, se unió a un relay, detectó NAT, ningún crash. El runtime de Go maneja ciertos faults de memoria por sí solo, pero el debugger los agarra primero. La solución fue un pequeño archivo de init de LLDB en el esquema de Xcode que pasa esas señales a Go.
“Folder path missing.” Después de volver a correr la app desde Xcode, mi carpeta sincronizada entró en estado de error. Claude sacó el archivo de config de mi teléfono y encontró la causa: iOS mueve el container de datos de una app a una ruta nueva cuando la app se reinstala, y Syncthing había guardado la ruta absoluta vieja. Los archivos seguían ahí; solo el puntero estaba obsoleto. Las rutas de carpeta ahora se guardan como ~/Documents/…, que Syncthing resuelve al container actual cada vez, y las configs rotas se auto-reparan al arrancar.
Ninguno de estos aparecería en una demo. Todos ellos aparecerían para los usuarios.
Trabajando con Claude
Quiero ser transparente sobre cómo se construyó esto, porque creo que el cómo es tan interesante como el qué.
Trabajé con Claude en mi terminal, como Claude Code. Yo di la dirección y tomé las decisiones de producto:
- qué debía ser la app,
- el pivot,
- la licencia open source,
- qué trade-offs eran aceptables.
Claude escribió la gran mayoría del código: Go, Swift, tests, build scripts, la landing page, e incluso el ícono de la app, dibujado con CoreGraphics porque los SF Symbols de Apple no se pueden usar en íconos.
Lo que lo hizo funcionar no fue la velocidad, aunque la velocidad fue notable. Fue verificación:
- Comprobó la fuente real. Las formas de endpoint vinieron de los archivos de documentación fuente de Syncthing, y los detalles de embedding del propio código Go de Syncthing, no de memoria.
- Cada cambio fue testeado. La suite ahora tiene 72 tests Swift más tests Go.
- El test principal es un sync real. El nodo embebido, corriendo dentro del simulador iOS, se parea con un proceso Syncthing separado, acepta una carpeta compartida a través del flujo normal de pending request, y sincroniza archivos en ambas direcciones.
- Miró lo que construyó. Tomó screenshots de la app y del sitio, encontró los defectos por sí solo (una imagen estirada, un campo desalineado, texto saliéndose de la página en teléfonos) y los arregló.
- Preguntó antes de actuar en las cosas que importaban: el pivot, la licencia, hacer público el repositorio.
También me dijo cosas que no había pedido escuchar: que mi primera especificación derrotaba su propio propósito, que open source significa elegir una licencia (no solo un repo público), y que una parte del puente depende de internals de Syncthing que podrían cambiar.
Open source, y devolver
SuperSynch se libera bajo la Mozilla Public License 2.0, la misma licencia que Syncthing. Syncthing y todas sus dependencias Go están incluidas sin modificación. Antes de elegir la licencia las auditamos: MIT, BSD, Apache-2.0 y MPL-2.0, nada incompatible. Están todas listadas en las third-party notices del repositorio.
“Syncthing” es una marca registrada de la Syncthing Foundation. SuperSynch es un proyecto independiente, no afiliado ni respaldado por la Foundation. Le escribí a la Foundation para contarles sobre el proyecto, preguntar si puede listarse entre las contribuciones de la comunidad, y ofrecer ayuda en el único lugar en que creo que el ecosistema podría mejorar: una API de embedding más estable. Los dos workarounds de arriba, arrancar el summary service y alcanzar el modelo, son exactamente el tipo de cosa que un pequeño cambio upstream podría volver innecesaria para toda app Syncthing móvil o embebida, no solo la mía.
Qué viene
- Archivos bajo demanda, descargando archivos individuales a través de la block API de Syncthing, y una File Provider extension como corresponde.
- Pedir la entitlement de multicast para que el descubrimiento local funcione en iOS.
- Más tiempo en dispositivos reales y redes reales: timing de background, batería y cambio de Wi-Fi.
- Eventualmente, TestFlight y App Store.
Si usas Syncthing y un iPhone o iPad, me encantaría que la probaras, la rompieras y me contaras lo que encuentres. Los issues y pull requests son bienvenidos en GitHub.
Y si todavía no usas Syncthing: empieza aquí. Corre en casi todo. Ahora, por fin, eso incluye el teléfono en mi bolsillo.
SuperSynch es open source bajo la MPL-2.0. Syncthing es una marca registrada de la Syncthing Foundation; este proyecto no está afiliado ni respaldado por ella.