Conversation
Replaces the Electron/NodeJS server with a Swift macOS application and CLI that read the
local iMessage database, serve the HTTP and Socket.IO API, and drive Messages.app for
sending. Feature-complete against the v1 surface; 2,139 tests in 409 suites.
**The contract is the v1 WIRE and nothing else.** Shipped clients cannot be updated in step
with the server, so the route table, response envelopes and event payloads a
default-configured server presents are fixed, and a parity harness enforces them against
recorded fixtures. Everything behind that is modelled on the reference rather than
transcribed from it: internals use whatever modern Swift design is best, and "the reference
does it this way" is not an argument for anything a client cannot observe. Four claims about
the reference that had been asserted in comments turned out to be false and are corrected in
place — each one was decoration on a decision that stood on its own merits.
Shape:
- Layered modules with the graph checked mechanically, so an undeclared import or an
unused dependency fails CI rather than review. Logic in BBInterfaces, controllers in
BBHandlers, wiring in BlueBubblesServerCore; services declare the settings and
capabilities they touch and an undeclared read throws. Setting keys are typed
everywhere, pinned by a test that refuses literals outside the registry.
- The container is assembled from the composition's four groups — storage, read path,
shared services, transport — and what is built on first use lives on its own actor.
Services and handlers take capability protocols; nothing takes the container.
- Live state is streamed, never polled: the registry publishes health on every
transition, the Private API runtime publishes its state, and the app follows both for
the life of the server.
- chat.db is opened read-only by construction and never written. Chat GUIDs are treated as
unstable and incomparable, because they differ between servers on one iCloud account and
macOS 26 rewrote every prefix.
- The Private API helper is injected into Messages and FaceTime with a socket inside each
app's own container, since the helper is sandboxed and cannot reach outside it. The
wire between them is typed: every field is a WireKey, and dispatch is split by the
roles the PrivateAPI protocol composes.
- Tailscale, ngrok and cloudflared are managed connection methods over one tool manager;
binaries are verified, version-checked, and never updated unattended.
- Secrets live in the data protection keychain, asserted at signing time: the release build
runs its own nested CLI and the build fails if it fell back to the legacy store. That
check cannot be a unit test — the entitlement comes from a signature and an embedded
provisioning profile, neither of which exists when tests run.
- Adopting an Electron installation is an explicit, resumable, user-driven wizard. Nothing
moves without a click, per-step state lives in app.db so the app and CLI agree, and a
headless install refuses to start rather than coming up silently on defaults with no
password.
- A login-item launcher supervises the app and owns restarts, because a process cannot
restart itself once it has died.
- The SwiftUI layer has no timer. Webhook deliveries and registrations, access control
including clock-based expiry, the published address and the log are all followed from
server streams; a value that lapses on the clock is the server's to announce rather than
the page's to re-read. Settings screens are generated from the registry, so declaring a
setting with a presentation is what puts it on screen.
- Switching an integration off means off everywhere. Contacts is gated at the interface,
so the API answers 403 naming the service instead of serving an index the user turned
off, and the app and the API can no longer disagree about what is running. The socket
can be switched off on its own while the REST API keeps serving, and cannot be left on
when the listener carrying it is off.
- What configures the HTTP listener sits under the HTTP listener: the bind address and TLS
termination are reached from Configure HTTP Settings rather than sitting loose among
settings that have nothing to do with either.
- A screen says which of "nothing yet", "nothing at all" and "it failed" it is looking at.
Every list page decided what to draw from whether its collection was empty, and a
collection is empty before the first result lands, so each announced its empty state for
the whole of the read. A scan refuses a screen that composes a read and never mentions
loading. The same confusion off a stopped service is fixed the same way: a tunnel
stranded by a switched-off dependency reports as stopped rather than reconnecting, so it
leaves the list of work in flight, and an address nothing is listening behind is not
offered as one to type into a phone.
- A symbol over an explanation is one component with a named tone. Four screens had drawn
that shape at three text sizes with the symbol in two places, each picking a colour for
itself — which is how a notice describing how a feature works ends up wearing the colour
of one reporting a problem.
- Prose in the Swift sources carries no em-dashes.
Deployment floor is macOS 14. Both configurations are supported and only one is capable:
without the Private API the server is limited to what AppleScript can do, so 60 of 148 routes
are gated and the capability is discoverable before a client tries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the Electron/NodeJS server with a Swift macOS application and CLI that read the
local iMessage database, serve the HTTP and Socket.IO API, and drive Messages.app for
sending. Feature-complete against the v1 surface; 2,139 tests in 409 suites.
The contract is the v1 WIRE and nothing else. Shipped clients cannot be updated in step
with the server, so the route table, response envelopes and event payloads a
default-configured server presents are fixed, and a parity harness enforces them against
recorded fixtures. Everything behind that is modelled on the reference rather than
transcribed from it: internals use whatever modern Swift design is best, and "the reference
does it this way" is not an argument for anything a client cannot observe. Four claims about
the reference that had been asserted in comments turned out to be false and are corrected in
place — each one was decoration on a decision that stood on its own merits.
Shape:
unused dependency fails CI rather than review. Logic in BBInterfaces, controllers in
BBHandlers, wiring in BlueBubblesServerCore; services declare the settings and
capabilities they touch and an undeclared read throws. Setting keys are typed
everywhere, pinned by a test that refuses literals outside the registry.
shared services, transport — and what is built on first use lives on its own actor.
Services and handlers take capability protocols; nothing takes the container.
transition, the Private API runtime publishes its state, and the app follows both for
the life of the server.
unstable and incomparable, because they differ between servers on one iCloud account and
macOS 26 rewrote every prefix.
app's own container, since the helper is sandboxed and cannot reach outside it. The
wire between them is typed: every field is a WireKey, and dispatch is split by the
roles the PrivateAPI protocol composes.
binaries are verified, version-checked, and never updated unattended.
runs its own nested CLI and the build fails if it fell back to the legacy store. That
check cannot be a unit test — the entitlement comes from a signature and an embedded
provisioning profile, neither of which exists when tests run.
moves without a click, per-step state lives in app.db so the app and CLI agree, and a
headless install refuses to start rather than coming up silently on defaults with no
password.
restart itself once it has died.
including clock-based expiry, the published address and the log are all followed from
server streams; a value that lapses on the clock is the server's to announce rather than
the page's to re-read. Settings screens are generated from the registry, so declaring a
setting with a presentation is what puts it on screen.
so the API answers 403 naming the service instead of serving an index the user turned
off, and the app and the API can no longer disagree about what is running. The socket
can be switched off on its own while the REST API keeps serving, and cannot be left on
when the listener carrying it is off.
termination are reached from Configure HTTP Settings rather than sitting loose among
settings that have nothing to do with either.
Every list page decided what to draw from whether its collection was empty, and a
collection is empty before the first result lands, so each announced its empty state for
the whole of the read. A scan refuses a screen that composes a read and never mentions
loading. The same confusion off a stopped service is fixed the same way: a tunnel
stranded by a switched-off dependency reports as stopped rather than reconnecting, so it
leaves the list of work in flight, and an address nothing is listening behind is not
offered as one to type into a phone.
that shape at three text sizes with the symbol in two places, each picking a colour for
itself — which is how a notice describing how a feature works ends up wearing the colour
of one reporting a problem.
Deployment floor is macOS 14. Both configurations are supported and only one is capable:
without the Private API the server is limited to what AppleScript can do, so 60 of 148 routes
are gated and the capability is discoverable before a client tries.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
Co-Authored-By: Claude Opus 5 noreply@anthropic.com