bare-macos
June 15, 2026 ยท View on GitHub
A peer-to-peer shared switch for macOS, showing how to embed Bare in a native SwiftUI app.
Flip the switch in one window and it flips in every other copy of the app - connected directly, device to device, with no server. The networking is JavaScript running inside the app on its own thread; the interface is native Swift. The two halves talk over a typed RPC protocol generated from a single schema.
This is an example/reference app, not a library - the macOS counterpart to bare-ios and bare-android.
What it demonstrates
- Embedding Bare in a native macOS app via bare-kit. The Bare runtime runs as a worklet - a JavaScript runtime on its own background thread, started and messaged by the native app.
- A real peer-to-peer stack on the desktop. Hyperswarm discovers peers through a distributed hash table and connects them with end-to-end (Noise) encryption - no server, no signalling.
- One core, shared across platforms. The worklet, the protocol (
schema.js), and the generated typed hrpc bindings (JS + Swift) live in bare-switch-core and are consumed by this app and by bare-ios. Neither side parses bytes by hand, and neither shell regenerates code - the core ships the committed bindings. - A distributed-systems lesson, on purpose. The shared switch is deliberately naive - last-writer-wins with no conflict resolution - so the demo can show you where that breaks and point you at the right tool for it (Autobase). See the two-act demo below.
Architecture
+------------------------------+ typed hrpc over IPC +------------------------------+
| SwiftUI (main thread) | <------- (bytes) ------> | Worklet thread (Bare) |
| Toggle, peer count, key | | Hyperswarm node |
| HRPC client (generated) | | HRPC server (generated) |
+------------------------------+ +--------------+---------------+
^ one schema generates both ends | Noise-encrypted
+------------------- schema.js -------------------+ v
| other copies of the app
| (found on the DHT)
The protocol lives once in bare-switch-core's schema.js, which generates both the Swift client and the JavaScript server under its spec/; this app consumes them. The switch state travels: toggle -> hrpc setState -> worklet -> broadcast to peers -> peers' worklets -> hrpc newState -> their UIs.
Quickstart
Prerequisites: macOS + Xcode, XcodeGen (brew install xcodegen), the GitHub CLI (gh, for the prebuilt framework), and Node.js.
One-time setup:
npm install # deps (incl. bare-switch-core) + build tools
# Fetch the prebuilt macOS BareKit framework:
gh release download v2.1.3 --repo holepunchto/bare-kit --pattern prebuilds.zip
unzip prebuilds.zip 'darwin/*' -d prebuilds/
mv prebuilds/darwin/BareKit.xcframework app/frameworks/
xcodegen generate # project.yml -> App.xcodeproj
Then the build is driven entirely by xcodebuild - the scheme's pre-actions re-link the native addons (bare-link) and re-pack the worklet (bare-pack, from bare-switch-core's backend.js) on every build:
xcodebuild -scheme App -derivedDataPath build build
To change the protocol or the worklet, edit bare-switch-core and rebuild; this app picks up the change through its dependency.
Try it - then watch it break (on purpose)
Act 1 - it syncs. Launch two copies of the built app:
open build/Build/Products/Debug/App.app # window 1
open -n build/Build/Products/Debug/App.app # window 2 (new instance)
Both start off. Flip the switch in one window and the other follows - instantly, with no server. That is the whole stack working: Hyperswarm found the peer on the DHT, opened a Noise-encrypted connection, and your flip crossed the native/Bare boundary as a typed hrpc call and back.
Act 2 - now break it. Quit both, then:
open build/Build/Products/Debug/App.app # one window - flip it ON while it is alone
open -n build/Build/Products/Debug/App.app # NOW launch the second window
The two windows disagree: the freshly launched peer's default clobbers the state you set. That is not a bug to file - it is the point. The switch is a shared mutable value with no ordering, so when two peers hold different states there is no way to know whose is "right." Last-writer-wins, and they can diverge.
The right tool for this: Autobase
Convergent multi-writer state is a solved problem in this ecosystem - it is just a different building block. Autobase linearizes each peer's append-only log into one deterministic view, so every peer ends in the same state regardless of join order, concurrent edits, or restarts. A real shared switch - or shared list, or collaborative document - would be built on it.
This example deliberately does not use Autobase: it brings storage, replication, and a multi-writer membership model that would bury the thing we are actually showing here - embedding Bare and talking to it over a typed protocol. Treat the divergence above as the motivation for reaching for Autobase next, not as a defect to patch here.
How it works
- Worklet (
bare-switch-core'sbackend.js) - runs on the Bare thread. It owns the Hyperswarm node and serves the hrpc interface. The bug-prone bit (local vs. remote changes, no-echo / no-loop) lives in the core'slib/switch.jsand is unit-tested there. - Transport (
app/BareTransport.swift) - bridges the generated hrpc engine to the worklet's IPC byte stream.bare-rpcdoes its own framing, so there is no hand-rolled byte parsing. - Model (
app/SyncModel.swift) - boots the worklet, wires the typed RPC, and exposes@Publishedstate to SwiftUI.
Project layout
app/ the SwiftUI app + IPC/hrpc transport
project.yml XcodeGen project spec (incl. Link/Pack pre-actions)
The worklet, schema, and generated bindings live in bare-switch-core.
Tests
The worklet's state logic is unit-tested in bare-switch-core. Here, the xcodebuild build is the integration test (the whole stack compiling and linking), and launching two instances that discover each other on the DHT is the end-to-end test.
Notes
- Entitlements.
project.ymldeclarescom.apple.security.cs.allow-jit(xcodegen writes it into the app's entitlements). It is required for V8's JIT under the Hardened Runtime on macOS; the same key is inert on iOS, where Bare runs V8 jitless. The app is not sandboxed; for the Mac App Store you would add the App Sandbox pluscom.apple.security.network.client/.server. - Minimum macOS. Targets macOS 13.0.
- Engine. Uses the default V8 build of BareKit; a JavaScriptCore variant also ships in the release.
License
Apache-2.0