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's backend.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's lib/switch.js and is unit-tested there.
  • Transport (app/BareTransport.swift) - bridges the generated hrpc engine to the worklet's IPC byte stream. bare-rpc does its own framing, so there is no hand-rolled byte parsing.
  • Model (app/SyncModel.swift) - boots the worklet, wires the typed RPC, and exposes @Published state 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.yml declares com.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 plus com.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