Build high-throughput, low-latency Apple device-to-device features without building a networking stack from scratch.
Loom is a Swift package for apps that need to find other devices, connect directly, verify identity, make trust decisions, and keep working when the local network is not the whole story.
It is designed for Apple platforms, stays product-agnostic, and gives you a clean base for the part every multi-device app eventually has to build. The transport is built for high-throughput, low-latency data movement between Apple devices, and the package includes a SwiftUI-first LoomKit surface for apps that want a plug-and-play integration path.
If you want the default integration path, start with LoomKit. Drop down to Loom only when you need to own discovery, advertising, handshake policy, or transport composition yourself.
Used in MirageKit.
If you are building something that talks to another device, you usually end up piecing together:
- discovery
- direct connections
- identity
- trust
- remote reachability
- diagnostics
Loom gives you those building blocks as a reusable Swift package.
That means you can focus on your app's behavior instead of spending weeks rebuilding networking plumbing.
Loom is a good fit for things like:
- Mac and iPhone companion apps
- local-first collaboration tools
- device control surfaces
- host and client apps on the same network
- pro apps that need to discover and connect to nearby machines
- products that start local but eventually need remote coordination
- One shared
LoomContainerper app or scene, modeled after SwiftData'sModelContainer - Main-actor
LoomContextinjected through SwiftUI environment values - Live
@LoomQuerypeer, connection, and transfer snapshots for SwiftUI lists - Actor-backed
LoomConnectionHandlevalues for message streams, file transfer, and custom multiplexed streams - Optional CloudKit-backed peer merging and relay-backed remote reachability without changing the app-facing API
- Nearby peer discovery over Bonjour, including peer-to-peer support
- Direct sessions built on
Network.framework - Stable device identity and key management
- Pluggable trust policy and local trust storage
- Seed-driven overlay discovery for Tailscale, Headscale, and other VPN-style networks
- Remote reachability support with relay presence and network probing
- Bootstrap tools for flows like Wake-on-LAN and SSH handoff
- Diagnostics and instrumentation hooks
- Loom-native interactive shell sessions over authenticated Loom transport
- macOS PTY host runtime for building a native host app quickly
- Connection policy that prefers Loom-native direct paths before SSH fallback
- Relay publication helpers for introducer-only remote access
- OpenSSH fallback runtime with password or private-key authentication
- Connection attempt reports that are usable in product UI
- CloudKit-backed peer sharing
- CloudKit-backed trust decisions
- Share and participant management for multi-device apps
This part matters, especially if you are new to this space.
Loom is the transport layer, not the product layer.
Loom does not decide:
- your app's protocol
- your message schema
- your UI
- your product roles
- your CloudKit schema naming
Your app owns those decisions. Loom gives you the network foundation underneath them.
For most apps, the practical comparison is LoomKit on top of Loom versus MultipeerConnectivity or a MultipeerKit-style convenience layer.
The main question is whether you want a convenient local-session API only, or a SwiftUI-first path that still has identity, trust, diagnostics, and remote growth underneath it.
| Capability | Loom |
MultipeerConnectivity |
|---|---|---|
| Networking model | Bonjour discovery plus direct Network.framework sessions you can reason about and extend |
High-level Apple-managed local peer sessions |
| Identity model | Stable device identity and signed session setup are first-class | Peer identity is mostly session-oriented and app-specific trust modeling is left to you |
| Trust decisions | Explicit trust providers and local trust storage | Invitation and certificate hooks exist, but there is no Loom-style trust layer to plug into your product |
| Remote growth path | Includes relay/STUN support and optional LoomCloudKit peer sharing and trust |
Focused on nearby/local networking with no built-in remote reachability story |
| Product boundaries | Keeps your protocol, schema, and app roles above the transport layer | Easy to start, but the framework shape tends to leak into the rest of your app architecture |
| Diagnostics and operability | Built-in diagnostics and instrumentation hooks | Much thinner observability surface |
| Best fit | Apps that need a durable multi-device architecture, not just nearby messaging | Quick local-first prototypes or simple nearby collaboration |
If your app only needs nearby discovery and a session quickly, MultipeerConnectivity is fine.
If you want the closest Loom equivalent to that convenience class of API, start with LoomKit.
If you need identity, trust, diagnostics, and a path beyond the local network, Loom's stack is the better foundation.
Loom can treat Tailscale and other overlay networks as direct connectivity instead of forcing those peers through relay-only flows. The model is intentionally simple: publish a small overlay probe listener on each Loom host, and provide LoomOverlayDirectory with the host names or IP addresses your app already trusts.
That means Loom does not depend on a specific control plane. You can feed the directory with MagicDNS names, stable overlay IPs, or results from your own inventory service:
let configuration = LoomContainerConfiguration(
serviceType: "_studio._tcp",
serviceName: "Studio Mac",
overlayDirectory: LoomOverlayDirectoryConfiguration(
probePort: Loom.defaultOverlayProbePort,
refreshInterval: .seconds(30),
probeTimeout: .seconds(2),
seedProvider: {
[
LoomOverlaySeed(host: "studio-mac.tailnet.example"),
LoomOverlaySeed(host: "100.64.0.25"),
]
}
)
)When a peer is visible through both the overlay and relay signaling, Loom prefers the direct overlay route and still preserves relay fallback if that host is temporarily unreachable.
Add Loom to your Package.swift:
dependencies: [
.package(url: "https://github.com/EthanLipnik/Loom.git", from: "1.4.0")
]Then add the product you want to your target:
For most apps, LoomKit should be the default dependency.
.target(
name: "MyApp",
dependencies: [
.product(name: "LoomKit", package: "Loom"),
// Or drop down to the lower-level primitives:
// .product(name: "Loom", package: "Loom"),
// Add this if you want the optional terminal/session layer:
// .product(name: "LoomShell", package: "Loom"),
// Add this too if you want CloudKit-backed peer sharing or trust:
// .product(name: "LoomCloudKit", package: "Loom"),
]
)If you want something in the MultipeerKit class of ergonomics, start with LoomKit.
LoomKit is modeled more like SwiftData than like raw networking services:
LoomContainerowns the runtimeLoomContextis the main-actor action surface@LoomQuerygives SwiftUI live snapshotsLoomConnectionHandleowns the long-lived async streams
import LoomKit
import SwiftUI
@main
struct StudioLinkApp: App {
let loomContainer = try! LoomContainer(
for: .init(
serviceType: "_studiolink._tcp",
serviceName: "Studio Mac",
deviceIDSuiteName: "group.com.example.studiolink"
)
)
var body: some Scene {
WindowGroup {
ContentView()
}
.loomContainer(loomContainer)
}
}import LoomKit
import SwiftUI
struct ContentView: View {
@Environment(\.loomContext) private var loomContext
@LoomQuery(.peers(sort: .name)) private var peers: [LoomPeerSnapshot]
var body: some View {
List(peers) { peer in
Button(peer.name) {
Task {
let connection = try await loomContext.connect(peer)
try await connection.send("hello")
}
}
}
.task {
for await connection in loomContext.incomingConnections {
Task {
for await message in connection.messages {
print("Received", message.count, "bytes")
}
}
}
}
}
}That is the intended default. You can add CloudKit-backed peer sharing, trust, and relay publication through LoomContainerConfiguration without changing the SwiftUI-facing API shape.
If you need full control over discovery, advertising, or handshake policy, drop down to Loom.
The main type there is LoomNode. It owns discovery, advertising, sessions, and the identity and trust collaborators you inject into it.
import Loom
let node = LoomNode(
configuration: LoomNetworkConfiguration(
serviceType: "_myapp._tcp",
enablePeerToPeer: true
),
identityManager: LoomIdentityManager.shared
)
let discovery = node.makeDiscovery()
discovery.onPeersChanged = { peers in
print("Peers:", peers.map(\.name))
}
discovery.startDiscovery()Use LoomNode when you want to own the full runtime boundary yourself. Use LoomKit when you want the repo to feel closer to SwiftUI + SwiftData.
LoomKitis the app-facing path for SwiftUI apps.LoomNodeis the lower-level transport composition root.LoomConnectionHandleandLoomAuthenticatedSessionare the live data paths.- Your app still owns protocol semantics, product policy, and UI behavior.
That split is what keeps Loom reusable instead of turning it into someone else's app framework.
- Swift 6.2+
- macOS 14+
- iOS 17.4+
- visionOS 26+
If you want the deeper material, go to the docs:
swift build
swift test --scratch-path .build-local