Skip to Content
Architecture

Architecture

Brewwery 1.1.1 is a native macOS application written in Swift 6 with SwiftUI, AppKit, and Foundation. There is no Electron, Node.js, Rust, web view, or JavaScript bridge. The native 1.x app replaces the 0.9.x Electron app.

Overview

┌───────────────────────────────────────────────┐ │ Brewwery (app target) │ │ SwiftUI views · @Observable feature models │ │ AppKit: menu bar item, Dock badge, windows │ │ Sparkle: application updates │ ├───────────────────────────────────────────────┤ │ BrewweryCore (library, no UI) │ │ HomebrewClient actor │ │ HomebrewCommand typed, validated argv │ │ HomebrewRunner actor, streaming │ │ OperationCoordinator one mutation at a time │ │ Parsers · Models · Persistence · Errors │ └───────────────────────────────────────────────┘ │ posix_spawn, no shell Homebrew (brew)

Project structure

Sources/ ├── BrewweryCore/ # no UI — testable on its own │ ├── Homebrew/ # detection, typed commands, validation, runner, coordinator │ ├── Models/ # Formula, Cask, OutdatedPackage, BrewService, … │ ├── Parsers/ # Homebrew JSON and text output, fixture-tested │ ├── Persistence/ # settings, history, favorites │ └── Errors/ Logging/ Utilities/ └── Brewwery/ # the app ├── App/ # entry point, delegate, environment, menus, operations ├── DesignSystem/ # palette, metrics, components, tables, state panels ├── Features/ # Shell (sidebar, status bar, search) and one folder per page ├── Models/ # @Observable feature models └── Catalogs/ # bundled Discover, command, and Brewfile data Tests/ # core, app, live, and shared test-support targets Packaging/ # Info.plist, entitlements, logo sources Scripts/ # app bundle, release, and screenshot scripts

The project is a Swift Package; open Package.swift in Xcode or use swift build.

Layers

Views and feature models

Each page is a SwiftUI view backed by an @Observable, @MainActor model — PackageLibrary, UpdatesModel, ServicesModel, and so on — owned by a single AppEnvironment. Models load data through HomebrewClient and publish loading, error, and empty states that the views render.

HomebrewClient

An actor that exposes one async method per Homebrew operation (search(_:), outdated(), services(), doctor(), …). It builds a HomebrewCommand, runs it, and turns the output into models with the parsers. Homebrew output that changed between versions is handled here — for example, falling back to brew list --versions --json when --json=v2 is rejected.

HomebrewCommand

A closed enum with one case per supported command. Each case validates its dynamic values with HomebrewIdentifier and returns a fixed argument list. Nothing else in the app can produce arguments for brew. See Security for the complete list and the validation rules.

HomebrewDetector

Finds brew in this order: a saved custom path, /opt/homebrew/bin/brew, /usr/local/bin/brew, then brew on PATH. A custom path is only used after it passes validation.

HomebrewRunner and ChildProcess

Runs brew with posix_spawn in its own process group, with the environment Homebrew needs plus HOMEBREW_NO_AUTO_UPDATE=1 and HOMEBREW_NO_ANALYTICS=1. Short commands capture their output; long operations stream standard output and standard error as an AsyncStream. Cancelling sends SIGTERM to the whole process group and SIGKILL five seconds later if needed, so child processes such as curl and git stop too.

Buffered commands are cancellable and enforce a 16 MB limit per pipe, returning an explicit error rather than truncated JSON. Reads have a 60-second timeout, short mutations five minutes, and custom-path probes ten seconds. Streaming pipes and pending UI output are bounded. The runner independently refuses overlapping mutations and waits for the child to exit before releasing its slot.

OperationCoordinator

Owns installs, uninstalls, upgrades, service actions, cleanup, metadata updates, tap changes, and pinning. It reserves a mutation slot before Homebrew detection starts, enforces operation timeouts, collects bounded output, and records results in History. Lists refresh after failures and cancellations as well as success. The guard is process-local: it does not serialize commands started from Terminal or another app.

Persistence

StoreBacking
SettingsStoreUserDefaults (com.brewwery.app)
HistoryStore~/Library/Application Support/Brewwery/history.json, newest 100 entries
FavoritesStore~/Library/Application Support/Brewwery/favorites.json

JSONPersistence coordinates snapshots on the main actor. JSON encoding, recovery copies, and atomic file writes execute on one serial utility queue. Each store keeps at most one in-flight write and the latest pending snapshot; a completed older write cannot erase a newer pending change.

Normal quit waits for pending saves using AppKit’s deferred termination reply. On failure, changes stay in memory and the UI offers Retry; quitting then requires a choice to discard them. Unreadable JSON is preserved as an .unreadable-<UUID>.bak file before replacement. Force-quit and power loss can still lose unsaved changes. Initial JSON loading and manual History export remain synchronous.

AppKit integration

AppDelegate installs the menu bar item, refreshes the Dock badge from the shared outdated list, and brings the main window forward from the menu. Application updates go through AppUpdateController. Sparkle requires a configured app bundle with a feed URL and public key; isolated QA profiles disable it. Distributed updates carry both Developer ID and Sparkle EdDSA signatures.

Errors

Every failure is a BrewweryError with a stable code, a user-facing message, and the raw output when there is one. Common codes:

CodeMeaning
HOMEBREW_NOT_FOUNDNo working Homebrew executable was found
BREW_COMMAND_FAILEDHomebrew exited with an error
BREW_JSON_PARSE_FAILEDHomebrew’s JSON output could not be read
INVALID_PACKAGE_NAME, INVALID_CASK_TOKEN, INVALID_TAP_NAME, INVALID_SERVICE_NAME, INVALID_FILE_PATHInput failed validation and was never sent to Homebrew
PACKAGE_INSTALL_FAILED, PACKAGE_UNINSTALL_FAILED, SERVICE_COMMAND_FAILED, CLEANUP_RUN_FAILED, …A specific operation failed
OPERATION_IN_PROGRESSAnother operation that changes Homebrew is still running
OPERATION_CANCELLEDYou cancelled the operation
OPERATION_TIMEOUTThe operation exceeded its safety timeout
BLOCKED_EXTERNAL_URLA link was outside the allowlist

Tech stack

AreaTechnology
LanguageSwift 6 with strict concurrency
UISwiftUI, with AppKit for the menu bar, Dock, and windows
StateObservation (@Observable)
ConcurrencySwift actors and AsyncStream
Processesposix_spawn with process groups
UpdatesSparkle 2
Loggingos.Logger, subsystem com.brewwery.app
TestsXCTest
BuildSwift Package Manager; universal arm64 + x86_64
Minimum OSmacOS 14 Sonoma
Last updated on