I’ve used Syncthing for years. It’s one of those rare pieces of software that does exactly one thing and does it right: it keeps folders in sync between your own devices, directly and end-to-end encrypted, with no cloud account and no company sitting in the middle. My MacBook, my servers and my backups all speak Syncthing.

My iPhone didn’t.

This is the story of how I fixed that in about two days, working with Claude, Anthropic’s AI coding assistant, and why the result, SuperSynch, is open source under the same license as Syncthing itself.

Why an open-source iOS version matters

Syncthing runs on Linux, Windows, macOS, the BSDs and more. On Android there are community apps. On iOS the situation has always been harder. There’s no official app, because Apple’s platform rules make a background sync daemon awkward: apps can’t run continuously, can’t freely scan the network, and live inside a sandbox.

There are good people working on this. Synctrain, for example, is an open-source iOS app built on Syncthing. But when I looked at what I wanted, a few things were clear:

  1. The phone should be a real Syncthing device. No remote control and no relay service of mine: the iPhone and the Mac should talk to each other directly, the same way my other machines do.
  2. It has to be open source. Software that holds my files, my device keys and my network connections is software I want to be able to read. And Syncthing is a commons; anything built on it should give back to that commons.
  3. I wanted to understand every layer of it. From the Go engine to the SwiftUI views, I wanted something I could maintain, explain and change.

So I decided to build one, and to do it in the open.

A false start, and why it was useful

My first specification was, frankly, wrong. I asked for a “remote controller”: a native app that talks to Syncthing instances running elsewhere (a NAS, a server, a Mac) through Syncthing’s REST API.

Claude built exactly that, and built it well:

  • a SwiftUI app for iPhone and iPad,
  • support for multiple servers,
  • API keys in the Keychain,
  • a trust-on-first-use flow for Syncthing’s self-signed HTTPS certificates (show the fingerprint, confirm, pin it),
  • a live dashboard driven by Syncthing’s event stream,
  • tests that ran against a real Syncthing instance.

Then I looked at it and realised it defeated the whole purpose. To use it, I would have had to expose my MacBook’s Syncthing GUI to my phone. And it still wouldn’t put a single file on the phone. I didn’t want a remote control. I wanted the phone to be a node.

So we pivoted. What surprised me was how much survived the pivot: the models, the event-processing logic, most of the UI. More on that below.

How it works: Syncthing, compiled for iOS

Syncthing is written in Go, and Go can be compiled into an iOS framework using gomobile. The plan became:

SwiftUI app  ──►  Swift client (async/await)  ──►  Go bridge  ──►  Syncthing v2.1.5
                                                   (gomobile)       (lib/syncthing)

Before writing any bridge code, Claude went and read Syncthing’s source to check that embedding was actually possible. It was, but with some twists that make a good story on their own.

The module path quirk. Syncthing’s v2 release tags aren’t usable with a plain go get. Go’s module system expects a /v2 suffix in the module path, and Syncthing’s isn’t named that way. The fix was to pin the exact v2.1.5 commit as a pseudo-version. Small thing, but the kind of detail that eats an afternoon.

The web GUI assets. lib/syncthing pulls in the web GUI package, whose assets are generated by Syncthing’s own build scripts. Syncthing has a noassets build tag for exactly this case, and since the embedded node doesn’t need a web GUI, everything is built with it.

The missing folder summaries. This one was subtle. In an early test, folders on the embedded node never updated their file counts. After digging through the source, we found the cause: Syncthing only starts its folder summary service, which emits the FolderSummary and FolderCompletion events, together with the web GUI. No GUI, no summaries. The bridge now starts that service itself.

Reaching the model. lib/syncthing deliberately exposes part of its internals for embedders. But some things the GUI shows aren’t in that subset: per-device connection statistics, pending devices remembered across restarts, folder statistics and file versions. The bridge reaches the full model through a small, documented piece of reflection. It’s verified by tests and flagged to re-check on every Syncthing upgrade. It’s the most pragmatic compromise in the project, and I’d love for it to become unnecessary (see below).

Keeping the Swift side boring

The design decision I’m happiest with: every piece of data crosses the Go→Swift boundary as JSON shaped exactly like Syncthing’s REST API. The bridge isn’t a REST server; nothing listens on the network. But its functions return the same shapes as /rest/db/status, /rest/system/connections, /rest/events and so on.

That meant the Swift code from the abandoned remote-control version carried over almost unchanged:

  • the decoding models (written to be lenient, because Syncthing’s API evolves),
  • the pure event reducer that turns Syncthing’s event stream into UI state,
  • the live session that follows events and falls back to polling.

The false start became the foundation.

One more detail: Go calls block. An event long-poll can sit waiting for up to its timeout. So the Swift wrapper runs them on a dedicated queue rather than Swift’s cooperative thread pool, and cancellation returns immediately instead of waiting for Go.

What the app does

  • Pairing: your iPhone shows its Syncthing device ID as a QR code; you scan or paste your Mac’s ID.
  • Files app integration: synced folders live in Files › On My iPhone › SuperSynch, usable by every other app. There’s also a built-in browser with Quick Look.
  • Selective sync: an ignore-patterns editor lets you keep only part of a large folder on the phone.
  • Versions and conflicts: file versioning with restore, and a conflict list where you choose which copy wins.
  • Untrusted devices: share a folder encrypted with a password.
  • Data-aware: Wi-Fi-only syncing, a Low Data Mode switch and bandwidth limits.
  • iPad: a three-column layout with keyboard shortcuts. iPhone and iPad share one codebase; only two small files differ.
  • No telemetry: Syncthing’s usage and crash reporting, auto-upgrade and web GUI are all switched off.

Being honest about iOS

I insisted that the README carry a clear Limitations section, because nothing erodes trust faster than an app that over-promises.

  • No continuous background sync. iOS won’t let an app run all the time. SuperSynch syncs while it’s open, finishes transfers for about 30 seconds after you leave, and uses the background time iOS grants, often while charging. In practice: open the app, and it catches up.
  • No local network discovery yet. Syncthing finds devices on a LAN with UDP broadcasts, which on iOS need a multicast entitlement that Apple has to approve. Until then, global discovery, relays or a manually entered address do the job.
  • Whole files, not on demand. Everything that isn’t ignored gets downloaded. On-demand files are on the roadmap.

Bugs that taught me something

Three moments from the build stand out.

“Don’t touch my real Syncthing.” Midway through, while testing in the simulator, Claude noticed that port 22000 was already taken, by the real Syncthing on my Mac, which had a live connection to another of my devices. The simulator shares the Mac’s network, so a careless test could have connected to it. It stopped, told me, and changed the setup so tests never touch default ports. In the simulator the app now uses its own port. I appreciated that it treated my machine as something to be careful with, not just a build environment.

The crash that wasn’t. On my iPhone, Xcode stopped with EXC_BAD_ACCESS. Instead of guessing, Claude launched the installed app on the phone without the debugger and streamed its console: Syncthing started, joined a relay, detected NAT, no crash. The Go runtime handles certain memory faults itself, but the debugger grabs them first. The fix was a small LLDB init file in the Xcode scheme that passes those signals through to Go.

“Folder path missing.” After I re-ran the app from Xcode, my synced folder went into an error state. Claude pulled the config file off my phone and found the cause: iOS moves an app’s data container to a new path when the app is reinstalled, and Syncthing had stored the old absolute path. The files were all still there; only the pointer was stale. Folder paths are now stored as ~/Documents/…, which Syncthing resolves to the current container every time, and broken configs repair themselves at startup.

None of these would show up in a demo. All of them would have shown up for users.

Working with Claude

I want to be transparent about how this was built, because I think the how is as interesting as the what.

I worked with Claude in my terminal, as Claude Code. I set the direction and made the product decisions:

  • what the app should be,
  • the pivot,
  • the open-source license,
  • which trade-offs were acceptable.

Claude wrote the vast majority of the code: Go, Swift, tests, build scripts, the landing page, and even the app icon, drawn with CoreGraphics because Apple’s SF Symbols can’t be used in icons.

What made it work wasn’t speed, although the speed was remarkable. It was verification:

  • It checked the real source. Endpoint shapes came from Syncthing’s documentation source files, and embedding details from Syncthing’s own Go code, not from memory.
  • Every change was tested. The suite now has 72 Swift tests plus Go tests.
  • The headline test is a real sync. The embedded node, running inside the iOS simulator, pairs with a separate Syncthing process, accepts a shared folder through the normal pending-request flow, and syncs files in both directions.
  • It looked at what it built. It took screenshots of the app and the website, found the flaws itself (a stretched image, a misaligned field, text running off the page on phones) and fixed them.
  • It asked before acting on the things that mattered: the pivot, the license, making the repository public.

It also told me things I didn’t ask to hear: that my first spec defeated its own purpose, that open source means picking a license (not just a public repo), and that one part of the bridge relies on Syncthing internals that could change.

Open source, and giving back

SuperSynch is released under the Mozilla Public License 2.0, the same license as Syncthing. Syncthing and all of its Go dependencies are included unmodified. Before choosing the license we audited them: MIT, BSD, Apache-2.0 and MPL-2.0, nothing incompatible. They’re all listed in the repository’s third-party notices.

“Syncthing” is a trademark of the Syncthing Foundation. SuperSynch is an independent project, not affiliated with or endorsed by the Foundation. I’ve written to the Foundation to tell them about the project, to ask whether it can be listed among the community contributions, and to offer help in the one place I think the ecosystem could improve: a more stable embedding API. The two workarounds above, starting the summary service and reaching into the model, are exactly the kind of thing a small upstream change could make unnecessary for every mobile or embedded Syncthing app, not just mine.

What’s next

  • On-demand files, downloading individual files through Syncthing’s block API, and a proper File Provider extension.
  • Requesting the multicast entitlement so local discovery works on iOS.
  • More time on real devices and real networks: background timing, battery and Wi-Fi switching.
  • Eventually, TestFlight and the App Store.

If you use Syncthing and an iPhone or iPad, I’d love for you to try it, break it and tell me what you find. Issues and pull requests are welcome on GitHub.

And if you don’t use Syncthing yet: start here. It runs on almost everything. Now, finally, that includes the phone in my pocket.


SuperSynch is open source under the MPL-2.0. Syncthing is a trademark of the Syncthing Foundation; this project is not affiliated with or endorsed by it.