Connections
Connections are how peers reach each other. In kuilt, each connection implementation is called a fabric.
Every fabric is wrapped in the same Loom/Seam contract, so from your app's point of view the workflow stays the same. Your app logic never needs to know whether peers are connected by WebSocket, LAN discovery, or direct radio.
Pick a connection path by deployment shape
Use WebSocket when you want the fastest path to cross-platform connectivity.
Use mDNS when peers need local-network discovery before connecting.
Use direct device links when you want peer-to-peer connectivity without a relay server.
WebSocket fabric (kuilt-websocket)
WebSocket is usually the quickest path to a cross-platform session. Setup is role-split (server accepts, client connects), but once connected both peers use the same Seam API. The split is:
KtorServerLoom— JVM/Android. Supports onlyhost();join()throws.KtorClientLoom— all targets. Supports onlyjoin();host()throws.
Server:
Client:
The advertisement includes the server's PeerId, so both ends get the same membership view without an extra handshake message.
mDNS discovery (kuilt-mdns, JVM/Android)
mDNS helps peers find each other on a local network. It is discovery, not transport: the session still runs over WebSocket. MDNSPeerLinkFactory registers an mDNS service on host() and resolves an MDNSAdvertisement to a WebSocket join on join(). Discover peers separately with MDNSServiceDiscoverer:
Limit your collection with a timeout or take(n), because discoveries() keeps emitting.
Direct device links
If you want direct device-to-device links, use these peer-to-peer fabrics. kuilt-multipeer (iOS/macOS) and kuilt-nearby (Android) are peer-to-peer with no relay server. They both implement Loom and use the same instance for host and join (one in-process mesh). Replace InMemoryLoom with one of these and your app code is unchanged.
kuilt-webrtc (wasmJs) provides a WebRTC data-channel fabric. WebRTC sessions need signaling, but that stays inside the fabric implementation — callers only see Loom/Seam.
Writing your own fabric
When your transport is not packaged yet, implement Loom (and a private Seam) and prove it behaves like every other kuilt fabric by subclassing SeamConformanceSuite.
Why this matters: conformance tests keep your custom fabric from surprising the layers above it.
newLoomPair() returns (hostLoom, joinerLoom). In-process radio fabrics return the same instance twice (shared mesh). Role-split fabrics (WebSocket, mDNS, WebRTC) return distinct host and joiner instances wired together. The suite runs host() and join() concurrently, which matters for WebSocket-style fabrics where host() suspends until a client connects.
The suite tests:
weave(Rendezvous.New(...))returns aSeamwith a non-emptyselfId.broadcastandsendTodeliver frames and stampsender.peerstracks membership.incomingis single-collection and ordered.close()is idempotent.availability()returns sensibly.
Keep real-network smoke tests in a separate test that is opt-in (e.g. -Pmy.fabric.integration.tests=true) so the conformance suite stays fast and deterministic.
Tag and custom discovery
Tag is an open interface. Each fabric defines its own (WebSocketAdvertisement, MDNSAdvertisement, …). A custom fabric provides a Tag with whatever its join() call needs.
The membership layer (kuilt-session)
Seam is pure transport — peers reflects whoever the wire says is connected. When your product needs room semantics (identified members, host role, reconnect behavior), add kuilt-session.
SeamRoomFactory wraps any Loom and produces Rooms with an admit/identify handshake, a roster of admitted members, reconnect tokens, and partition detection:
Because SeamRoomFactory accepts any Loom, the same code runs over InMemoryLoom in tests and over WebSocket or mDNS in production.