why
I’ve been using Soulseek for a very long time. If you’re not familiar, it’s a peer-to-peer file sharing network that’s been around since the early 2000s — primarily used by music nerds / DJ’s to share rare recordings, obscure albums, and high quality lossless files you can’t find anywhere else. It’s one of those corners of the internet that somehow survived every platform shift and streaming takeover, mostly because the community genuinely cares about preservation and discovery in a way that your preferred DSP’s algorithm never will.
The problem is that the existing clients are terrible. On macOS your options are basically Nicotine+ (a Python/GTK app that looks and feels like it’s running on Linux circa 2008), or the horriffic official SoulseekQT client. Neither of these are good experiences. I use my Mac for everything, I care about how my tools feel, and I wanted something that actually belonged on the platform.
So I built seeleseek.
the protocol
The Soulseek protocol is completely undocumented. There’s no RFC, no spec sheet, no official documentation of any kind. The only real references are the Nicotine+ source code (Python) and some scattered community documentation from the Nicotine+ team. So the first step was essentially reverse engineering a 20+ year old binary TCP protocol by reading someone else’s Python implementation and translating it into Swift.
The Soulseek protocol is actually two protocols in one. There’s the server connection — you connect to a central server that handles authentication, search routing, chat rooms, and user discovery. Then there’s the peer-to-peer layer, where clients connect directly to each other for file transfers, browsing shares, and search result delivery. On top of that there’s a distributed search network where clients form a hierarchy to propagate searches across the network without the server being a bottleneck.
All of this is raw binary over TCP. Every message is a length-prefixed blob of packed integers, strings, and nested lists. No JSON, no protobuf, no HTTP. Just bytes on a wire.
I implemented the entire protocol from scratch in Swift. The message parser handles binary data extraction with proper bounds checking and security limits. The message builder constructs outgoing protocol messages. And the connection layer uses Apple’s Network framework directly — NWConnection for raw TCP sockets (because this kind of protocol-level work doesn’t belong anywhere near URLSession).
swift 6 and actors
I made the decision early on to go all-in on Swift 6.x strict concurrency. Every connection is an actor. The server connection, each peer connection, the database layer, the NAT service — all actors. The app state uses @Observable with @MainActor for the UI layer.
A peer-to-peer app is inherently concurrent — you might have dozens of simultaneous connections, file transfers happening in parallel, search results streaming in from multiple peers, chat messages arriving at any time. Without actors, this is a threading nightmare. Race conditions everywhere. With Swift 6, the compiler catches data races at build time. The entire class of “it works fine until 50 people are transferring at once and then it crashes” bugs just doesn’t exist.
The tradeoff is that Swift 6 concurrency can be incredibly painful to work with. Sendable conformance, actor isolation boundaries, the occasional need for nonisolated or Mutex for synchronous reads, it adds friction. But for this kind of app, the safety is worth it.
nat traversal
One of the more interesting challenges was NAT traversal. Most users are behind routers that block incoming connections, which is a problem when you need peers to connect directly to each other for file transfers. Seeleseek implements three layers of NAT traversal:
UPnP — the app discovers your router via SSDP multicast and requests a port mapping. This works on most consumer routers and is the cleanest solution.
NAT-PMP — a simpler protocol that Apple’s AirPort routers used. Sends UDP packets to port 5351 on your gateway.
Firewall piercing — when all else fails, the Soulseek server acts as a relay to help two firewalled peers establish a connection. One peer sends a ConnectToPeer message through the server, and the other responds with a PierceFirewall handshake.
Getting all three to work reliably, with proper fallbacks and without triggering your router’s intrusion detection system, was its own mini-project.
the database
Every good native app needs persistence. Search history, transfer queues that survive app restarts, chat logs, cached browse results. I went with GRDB over Core Data because I wanted direct SQL control without the overhead and headaches of an ORM. GRDB gives you SQLite with proper migrations, connection pooling (WAL mode for concurrent reads), and just enough abstraction to keep things clean.
Transfers can resume from where they left off. Search results are cached with a TTL so you’re not hammering the network. Browse results (a user’s complete shared file tree) are cached for 24 hours because fetching them is expensive. The whole thing uses a repository pattern that keeps the database concerns separate from the UI state.
what i learned
Building seeleseek taught me more about networking in swift than any other project i’ve done. There’s something about implementing a protocol at the byte level that forces you to actually understand what’s happening on the wire. You can’t hide behind abstractions when you’re manually packing integers into a Data buffer.
It also reinforced something I already believed: native apps are always worth the effort. Seeleseek is a SwiftUI app that uses platform conventions; native keyboard shortcuts, proper menu bar integration, launch-at-login through ServiceManagement. It feels like it belongs on macOS in a way that an Electron or Tauri wrapper never would. That matters to me, and perhaps it matters to the kind of people who still use Soulseek.
The app is open source on GitHub if you want to check it out or contribute. Signed builds are available on the releases page.