Under the hood
Architecture Design
HoneyContainer is a native macOS app built with SwiftUI. Here's how it's structured internally—from the presentation layer down to credential storage.
Effective September 26, 2026
Overview
HoneyContainer follows a layered, modular architecture with clear separation between presentation, state management, business logic, and system integration. It targets macOS 26.0+ on Apple Silicon and is written in Swift 6.3.
Design goals: an intuitive multi-pane interface for complex container operations, non-blocking async execution, testable business logic via protocol abstraction, encrypted credential storage, and reactive state updates throughout the UI.
1. Architecture layers
The application is organized into five layers, each with a single responsibility. Data and commands flow down through the stack; state changes propagate back up through reactive bindings.
PRESENTATION SwiftUI views (ContentView, SidebarView, DetailPanelView...)
| observes / updates
STATE MANAGEMENT AppState, MenuPresentationState, @Published collections
| commands / mutations
BUSINESS LOGIC Managers & services (AWS credential proxy, CLI runners...)
| orchestrates / executes
INFRASTRUCTURE CLIProcessRunner, Logger, FileManager, Network APIs
| reads / writes
DATA UserDefaults, Keychain, FileSystem, SecureStorage| Layer | Responsibility | Key components |
|---|---|---|
| Presentation | Render UI, capture input. Pure SwiftUI—no business logic. | ContentView, SidebarView, CenterPanelView, DetailPanelView, feature views |
| State management | Single source of truth, reactive updates, thread-safe on the main actor. | AppState, MenuPresentationState |
| Business logic | Container operations and workflows behind protocol seams for testability. | AWS credential proxy, per-runtime CLI wrappers (ContainerCLI, DockerCLI, PodmanCLI, KubectlCLI), build orchestration |
| Infrastructure | Low-level system access: processes, logging, files, networking. | CLIProcessRunner, Logger, SecureStorage |
| Data | Persistence and encrypted credential storage. | UserDefaults, Keychain, FileSystem |
2. Component structure
Entry point and top-level composition:
HoneyContainerApp (@main)
-> creates/injects AppState (@StateObject)
-> ContentView
+- SidebarView (navigation)
+- CenterPanelView (primary content)
+- DetailPanelView (secondary detail)Feature areas are organized around the same pattern of views paired with business logic:
- Containers & images — ImagesListView, ImageDetailView, a unified ContainersListView tree covering Apple container, Docker, Podman, and Kubernetes rows, ImageBuildLogViewer, and KubectlApplyView for manifest apply/diff
- Logging & monitoring — LogView, StatsView, metrics polling and log streaming
- AWS integration — AWSProfilesView, AWSCredentialsNewView, AWSLaunchOptionsView, backed by AWSCredentialProxyHelperManager
- Registry management — RegistryView, RegistryLoginView, credential caching and authentication
- Execution environment — EnvView, EnvListView, CommandView, environment variable management
- Remote host connectivity — RuntimesView for local and remote runtimes, RemoteHostNewView, remote system diagnostics, and RemoteHostStore/RemoteHostTrustStore, backed by SSH/SFTP via Citadel with host fingerprint verification
3. Key design patterns
| Pattern | How it shows up |
|---|---|
| MVVM | AppState acts as the view model: @Published properties plus methods that mutate state; SwiftUI views observe and render. |
| Observer (Combine) | User Action -> AppState method -> mutate @Published property -> SwiftUI re-renders. |
| Protocol-based dependency injection | Seams like CLIProcessRunning let RealCLIProcessRunner ship in production and FakeCLIProcessRunner stand in for tests. |
| Singletons | Shared resources such as Logger.shared and the AWS credential proxy manager owned by AppState. |
| Main actor isolation | AppState is @MainActor, so all published-state mutations are guaranteed to happen on the main thread. |
| Error translation | Infrastructure errors (e.g. a CLI timeout) are translated into application-level errors like ContainerCommandError before reaching the UI. |
| Structured concurrency | Async work runs in `.task {}` or `Task {}` blocks rather than callbacks or manual thread management. |
4. Data flow
A typical user action, such as deleting a container, flows straight down the layer stack and back:
User clicks "Delete Container" -> View calls appState.deleteContainer(id:) -> AppState: set isLoading, optimistically update collection -> Business logic formats CLI arguments, calls CLIProcessRunner -> Infrastructure spawns Foundation.Process, captures stdout/stderr -> Result returns up the stack -> AppState updates @Published state (or restores it on error) -> SwiftUI re-renders affected views
Real-time data such as container stats and logs use a polling loop instead of a one-shot round trip:
Task { @MainActor in
while !Task.isCancelled {
let containers = try await fetchContainers()
self.containers = containers
try await Task.sleep(for: .seconds(2))
}
}5. State & UI architecture
AppState is the central, @MainActor-isolated state container. It loads configuration from UserDefaults on init, publishes changes via Combine, starts/stops background refresh tasks, and captures errors for presentation to the user.
The UI is a menu-driven multi-pane layout, not a navigation stack: a persistent sidebar selects a SidebarItem, the center panel switches on that selection, and an optional detail panel shows contextual information. Views never talk to each other directly—they read and write through AppState, and SwiftUI’s onChange bindings keep everything in sync.
6. System integration
- Process execution — CLIProcessRunner spawns Foundation.Process to drive the
container,docker,podman, andkubectlCLIs, AWS CLI calls, and image builds. HoneyContainer talks to Apple’s container runtime exclusively through thecontainerbinary; the earlier in-process API client mode and its package dependencies have been removed. - File system access — app caches and preferences, user-selected directories via NSOpenPanel, and the Docker socket at
/var/run/docker.sock. - Network access — outbound-only client connections for registry pulls/pushes, AWS API calls, and remote SSH hosts via Citadel.
- Keychain — AWS access keys, registry credentials, SSH keys, and symmetric keys are stored via a
KeychainSymmetricKeyStoreabstraction. - Logging — unified logging to the console and rolling log files in
~/Library/Caches/, with ISO 8601 timestamps.
7. Multi-runtime container sources
The unified Containers tree merges rows from four independent CLI-backed sources: Apple container, Docker, Podman, and Kubernetes. Each runtime instance selects its source, and each source has its own wrapper (ContainerCLI, DockerCLI, PodmanCLI, KubectlCLI) that shells out to its binary and maps the output to a shared ContainerRow model.
Apple container / Docker container (flat) Podman pod -> pod member (one level) Kubernetes namespace -> pod -> pod member (two levels)
- Podman has a first-class pod concept with no Docker equivalent.
pod psandps -arun concurrently and are merged into a tree of pods and their member containers. - Kuberneteshas no independently listable container resource, so a pod’s containers are synthesized from
kubectl get pods --all-namespaces -o jsonand nested under namespaces and pods. - Row IDsare prefixed per source so they can’t collide across runtimes, and so a Kubernetes pod’s or container’s namespace and name can be recovered from the ID alone.
- Remote hosts follow the same split: each host dispatches to the CLI that matches its configured runtime.
Kubernetes manifest apply — KubectlApplyView lets you paste or pick YAML/JSON manifests and run a server-side dry-run, kubectl diff, or a real kubectl apply, all namespace-scoped. Syntax is checked client-side first with Yams; since YAML 1.2 is a JSON superset, one parser covers both formats.
8. Concurrency model
HoneyContainer uses Swift’s structured concurrency throughout: .task {} for view-attached work, explicit Task {} for background operations, and @MainActor isolation to enforce that UI-affecting state only ever mutates on the main thread. Sendable types (e.g. CLICommandResult) keep data safe as it crosses thread boundaries, and background polling loops check Task.isCancelled so they shut down cleanly when the app terminates.
9. Security architecture
- Credential storage— AWS keys and registry credentials are stored as Keychain generic password items, scoped to the app’s Team ID and Bundle ID, and are never written to disk unencrypted. A single master key in the Keychain (one permission prompt on first launch) derives purpose-specific sub-keys via HKDF-SHA256.
- Secure communication — HTTPS via swift-nio-ssl and the macOS Security framework; SSH via Citadel; certificate validation via Swift Certificates.
- Input validation — form validators sanitize file paths, image names, environment variables, network subnets, volume driver options, and labels before they reach a CLI invocation.
For the full security policy, including vulnerability reporting, see the Security Policy.
10. Extension points
The layered design makes it straightforward to add new functionality without touching unrelated code:
- New container operations (e.g. pausing a container) add a method and published property to AppState, a button in the relevant view, and a CLI call in the business logic layer.
- New registry types implement a
RegistryProviderprotocol and register a matching view in RegistryView. - Telemetry can be added behind a protocol seam and injected into AppState without changing existing call sites.
11. Architecture decisions
| Decision | Rationale |
|---|---|
| SwiftUI over AppKit | Modern toolkit, better composition, easier maintenance. |
| Centralized AppState | Single source of truth reduces bugs and simplifies debugging. |
| Protocol abstraction for CLI calls | Enables testing without a real Docker or container runtime. |
| Main actor isolation | Prevents data races and enforces correct threading by construction. |
| UserDefaults for configuration | Lightweight, persists across app updates in the same domain. |
| Keychain for secrets | The industry-standard, encrypted-at-rest option on macOS. |
Related pages
Want the feature-level view instead of the internals? See the full feature list, the software bill of materials, or the security policy.
