Skip to content

feat(swift): a native Swift BlueBubbles server - #846

Closed
zlshames wants to merge 1 commit into
masterfrom
claude/swift-server-migration-plan-wmoxm6
Closed

zlshames wants to merge 1 commit into
masterfrom
claude/swift-server-migration-plan-wmoxm6

Conversation

@zlshames

Copy link
Copy Markdown
Member

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

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>
@zlshames zlshames closed this Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant