Architecture¶
Note: This is a short summary. ARCHITECTURE.md is the authoritative and complete description, including the module map.
Process boundaries¶
blueferry-backendowns MAP, PBAP, ANCS, contact resolution, thread identity, history retention, notification policy, and the D-Bus API. It runs unprivileged and has no sudo path.- GTK, Qt, TUI, Quickshell, and the CLI message commands are replaceable clients. They receive opaque thread keys and can't construct different recipients for an existing thread.
- Inside the backend,
dbus_serviceis only a wire adapter overbackend_operations.daemonorchestrates lifecycle,event_dispatcherfans events out to sinks, andprofile_supervisorowns MAP/PBAP transitions.
Bluetooth¶
| Profile | Transport | Modules |
|---|---|---|
| MAP | Classic, OBEX | obex/ (sessions, worker, events, send, read) |
| PBAP | Classic, OBEX | contacts.py, contact_sync.py, contact_repository.py |
| ANCS | LE, GATT | ancs/ (client, parsers, sequencer) |
- One worker thread with its own bus connection serializes every blocking OBEX operation, so the GLib main loop never blocks.
bearer_supervisorconnects Bluetooth Classic first, then keeps LE connected alongside it.solicitation_supervisorkeeps the ANCS advertisement on air until ANCS is proven healthy.adapter_class_supervisorrepairs Class-of-Device drift through a fixed systemd helper allowed by a narrow Polkit rule.bluetooth_recoveryis a rate-limited, last-resort adapter power cycle for persistent ANCS outages.
Pairing¶
pair_setup is the low-level layer, and setup_client exposes typed
operations to GTK, Qt, and the CLI wizard. Quickshell reaches the same
operations through pairing-* JSON helpers. pairing_policy resolves two
independent axes instead of a table of device quirks:
- Delivery mode: full (MAP, PBAP, ANCS) or compatibility (MAP and PBAP
only,
BLUEFERRY_ANCS_ENABLED=false). - Authentication: Connect-first, letting the iPhone start
authentication, or explicit
Device1.Pair()for controllers that cancel Connect-first.
Confirmation in the initiating client is mandatory; no caller falls through to a desktop Bluetooth agent.
Pairing sequence (full mode, Connect-first)¶
sequenceDiagram
actor User
participant Client as BlueFerry client
participant BlueZ
participant iPhone
participant Daemon as Backend daemon
Client->>BlueZ: Register device-scoped pairing agent
Client->>BlueZ: Device1.Connect() on the unpaired iPhone
BlueZ->>iPhone: Classic connection
iPhone->>BlueZ: Starts authentication itself
BlueZ->>Client: Numeric comparison
Client->>User: Confirm the code
User->>iPhone: Confirm the same code
Note over BlueZ,iPhone: One bond covers Classic and LE<br>(LE keys derived over BR/EDR)
Client->>BlueZ: Trust bond, prefer BR/EDR, let Classic settle
Client->>BlueZ: Register ANCS solicitation advertisement
Client->>Daemon: Start backend
Daemon->>iPhone: MAP and PBAP over Classic
iPhone-->>User: Shows the Messages and Contacts switches
Daemon->>iPhone: LE connection, subscribe to ANCS
Daemon->>iPhone: Control Point probe (Messages app name)
iPhone-->>Daemon: Data Source response, ANCS ready
Success means MAP/PBAP work end to end, not only that a bond exists. The behavior behind each step is documented in PROTOCOL.md.
Storage and privacy¶
- Everything from the iPhone is untrusted input. Parsers, transfers, and
retained payloads have explicit limits in
limits.py. - History and the contact cache are owner-only SQLite files. Sensitive records are encrypted with AES-256-GCM under a random key held by the Secret Service. Clients never see the key.
- Storage fails closed: a missing or wrong key makes storage unavailable without deleting records, while live delivery continues.
- Logs never contain message bodies, notification text, or recipient identities.
Clients¶
- All Python clients decode the API through
client_wireintomodels, and share conversation and reply logic inconversation_state. - GTK uses a worker-owned bus connection, Qt a
BridgeControlleron a thread pool, the TUI Textual workers. Quickshell talks to one persistentquickshell_bridgeprocess over stdin. - Some Quickshell rules are re-implemented in QML (
OnboardingState.qml,ConversationLogic.qml). A change to those rules has to be made in both places.
Growth rules¶
- Keep Bluetooth and storage ownership in the backend.
- Add protocol behavior behind the D-Bus boundary before adding UI.
- Change versioned interfaces only compatibly and update the XML and client models in the same patch.
- Keep
Events1signals content-free. - Never block the daemon's main loop: asynchronous D-Bus calls only, and
blocking work on the OBEX worker.
tools/lint_mainloop.pychecks this.
Each rule is explained in ARCHITECTURE.md.