Architecture follows the traffic
A network debugger has three very different jobs. It needs a UI that can help humans reason about traffic. It needs a runtime that can accept client connections, handle HTTP and CONNECT tunnels, decrypt eligible HTTPS traffic, apply rules, forward upstream, and store results. It also needs a privileged boundary for system-level proxy and certificate operations.
The Rockxy app docs describe those as three domains: UI and orchestration, proxy/runtime services, and a privileged helper. That split is not only tidy architecture. It is a security decision. The code that renders an inspector should not be the same layer that mutates system proxy state.
Evidence from the source tree
These are the repo entry points behind the architecture described in this post:
- ProxyServer.swift owns the SwiftNIO server bootstrap and handler pipeline.
- RequestTableView.swift bridges AppKit
NSTableViewinto SwiftUI for the high-volume request list. - CertificateManager.swift owns the root CA lifecycle and per-host certificate cache.
- ConnectionValidator.swift validates helper callers over XPC.
- BreakpointRequestBuilder.swift centralizes safe request rebuilding after user edits.
SwiftUI for product surfaces, AppKit where scale demands it
SwiftUI is a good fit for many Rockxy surfaces: windows, inspectors, settings, setup flows, sidebars, and state-driven panels. The declarative model makes it easier to keep product state readable and consistent.
But a proxy request list is a high-volume macOS data view. It can receive thousands of entries while the user scrolls, filters, selects rows, and opens the inspector. Rockxy uses AppKit where that control is valuable, including high-volume table behavior through native macOS views bridged into the SwiftUI app.
This hybrid approach keeps the product native without forcing every problem through one UI abstraction. SwiftUI carries the modern app structure. AppKit handles the places where mature macOS controls fit the workload better.
The request list is a good example. The implementation uses NSViewRepresentable to place an AppKit table inside the SwiftUI app, then keeps selection, sorting, double-click, menus, and column state in a coordinator.
struct RequestTableView: NSViewRepresentable {
let rows: [RequestListRow]
@Binding var selectedIDs: Set<UUID>
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSScrollView()
let tableView = NSTableView()
tableView.dataSource = context.coordinator
tableView.delegate = context.coordinator
tableView.doubleAction = #selector(Coordinator.handleDoubleClick(_:))
return scrollView
}
final class Coordinator: NSObject,
NSTableViewDataSource,
NSTableViewDelegate,
NSMenuDelegate
{
func numberOfRows(in tableView: NSTableView) -> Int {
rows.count
}
}
}
Source: RequestTableView.swift. The comments in that file explicitly call out why Rockxy uses NSTableView: virtual scrolling, cell reuse, and column sorting for 100k+ rows.
SwiftNIO gives the proxy a real pipeline
A local debugging proxy has to understand more than "send request, receive response." It accepts inbound connections, distinguishes plain HTTP from CONNECT tunnels, terminates client TLS when interception is enabled, opens upstream TLS connections, applies rules and plugins, records timing, captures bodies, and forwards data without blocking the UI.
SwiftNIO is the right shape for that job because it gives Rockxy an event-driven channel pipeline. Different handlers can own different phases: HTTP parsing, CONNECT handling, TLS interception, upstream relay, rule evaluation, plugin hooks, traffic capture, and storage handoff.
That pipeline also makes future protocol work more practical. Richer WebSocket lifecycle, HTTP/2, gRPC, and protocol-aware AI or Web3/RPC debugging all benefit from explicit transport and protocol boundaries. Direction still needs implementation and tests, but the architecture leaves room for it.
The proxy bootstrap shows the boundary clearly: server setup is actor-isolated, while SwiftNIO handles I/O concurrency through an event loop group and a channel pipeline.
actor ProxyServer {
func start() async throws {
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let bootstrap = ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(ConnectionTimeoutHandler(timeout: .seconds(300))).flatMap {
channel.pipeline.addHandler(ConnectionLogger())
}.flatMap {
channel.pipeline.configureHTTPServerPipeline()
}.flatMap {
channel.pipeline.addHandler(HTTPProxyHandler(
certificateManager: certManager,
ruleEngine: ruleEng,
scriptPluginManager: scriptMgr,
onTransactionComplete: callback,
onBreakpointHit: breakpointHit
))
}
}
.childChannelOption(.maxMessagesPerRead, value: 16)
}
}
Source: ProxyServer.swift. The handler split continues through HTTPProxyHandler.swift, TLSInterceptHandler.swift, and HTTPSProxyRelayHandler.swift.
Actors keep runtime state off the main thread
Network capture is concurrent by nature. Multiple clients can connect at once. Requests can complete out of order. Large bodies can arrive while the user is filtering a previous session. The main thread should not serialize all of that work.
Rockxy's architecture docs describe actors for proxy, certificate, log, and session behavior. That gives concurrent subsystems explicit ownership over state: the proxy server owns its event loop and bootstrap, the certificate manager owns root and host certificate cache, traffic/session managers own buffers and persistence handoff, and the UI coordinator stays on the main actor.
This is especially important for security. When a subsystem owns its boundary, it becomes easier to validate inputs and reason about mutation. Certificate issuance should go through certificate code. Breakpoint edits should go through request rebuilding. Storage should go through session persistence. UI selection should not quietly become a network policy change.
The certificate and storage layers are both actors. The certificate manager serializes access to root CA state and the host certificate cache; the in-memory session buffer caps live memory while preserving request-list order.
actor CertificateManager {
static let shared = CertificateManager()
private(set) var rootCAFreshlyInstalled = false
private(set) var lastTrustValidationResult: Bool?
func generateRootCA() throws {
let result = try RootCAGenerator.generate()
rootCACertificate = result.certificate
rootCAPrivateKey = result.privateKey
try CertificateStore.saveRootCACertificate(result.certificate)
try CertificateStore.saveRootCAPrivateKey(result.privateKey)
}
}
actor InMemorySessionBuffer {
init(maxCapacity: Int = 50_000) { self.maxCapacity = maxCapacity }
func append(_ transaction: HTTPTransaction) {
transactions[transaction.id] = transaction
orderedIds.append(transaction.id)
evictIfNeeded()
}
}
Sources: CertificateManager.swift and InMemorySessionBuffer.swift.
Secure code structure starts with named boundaries
Rockxy's app repo organizes code around the actual risk areas: proxy engine, certificate management, rule engine, log engine, traffic capture, storage, detection, plugins, services, models, views, and shared helper protocols.
That structure gives reviewers places to look. TLS trust questions live near certificate code. Traffic mutation questions live near rules, breakpoints, and plugins. Persistence questions live near storage. UI state questions live near the coordinator and views. Helper trust questions live near shared XPC validation and helper code.
Security rarely improves when everything is "just app code." It improves when dangerous capabilities are named, isolated, validated, and tested.
The helper boundary is one of the clearest examples. The public helper entry point validates the incoming XPC caller before privileged work runs, and the validator performs both bundle identity and audit-token-backed checks.
enum ConnectionValidator {
static func isValidCaller(_ connection: NSXPCConnection) -> Bool {
let auditTokenData = extractAuditTokenData(from: connection)
return validateCaller(
pid: connection.processIdentifier,
auditTokenData: auditTokenData
)
}
static func validateCaller(pid: pid_t, auditTokenData: Data?) -> Bool {
let pidValid = CallerValidation.validateCaller(
pid: pid,
allowedIdentifiers: allowedCallerIdentifiers
)
guard pidValid else { return false }
// If audit token data is available, recheck identity from the token.
return true
}
}
Source: ConnectionValidator.swift. Tests live in ConnectionValidatorTests.swift.
Rules, breakpoints, and scripts are controlled mutation points
A debugger is not only a viewer. Engineers need to block hosts, throttle connections, map local files, map remote hosts, rewrite headers, pause requests, edit responses, and replay modified inputs. Those features are useful because they change traffic.
The cost is that mutation points need strict structure. Header values need sanitization. Breakpoint edits need centralized rebuilding so content length, authority, scheme, and body match. Map Local needs path containment. Regex rules need validation and caps. Script hooks need a bounded API, timeout enforcement, and no direct system access.
That is why Rockxy treats mutation as a product surface and an engineering boundary. A rule is not a random callback buried in UI code. It is policy applied in the proxy pipeline.
Breakpoint edits are rebuilt through a single utility instead of ad hoc request mutation inside each proxy handler. That makes host pinning, scheme preservation, and body/header reconciliation testable.
enum BreakpointRequestBuilder {
static func build(
from modifiedData: BreakpointRequestData,
originalHead: HTTPRequestHead,
originalRequestData: HTTPRequestData,
isHTTPS: Bool = false,
originalHost: String? = nil
) -> Result {
// Preserve authority for path-only edits.
// Pin HTTPS Host to the CONNECT tunnel authority.
// Reconcile Content-Length with the edited body.
var head = originalHead
head.method = HTTPMethod(rawValue: modifiedData.method)
head.headers = HTTPHeaders(resolvedHeaders.map { ($0.name, $0.value) })
return Result(head: head, requestData: requestData)
}
}
Source: BreakpointRequestBuilder.swift. Tests live in BreakpointRequestBuilderTests.swift.
Script plugins are also actor-managed. Startup loading is one-shot, concurrent callers share the same in-flight task, and a nonisolated snapshot lets NIO event-loop code make pre-hook decisions without awaiting into plugin state on the hot path.
actor ScriptPluginManager {
nonisolated static let pluginSnapshot =
OSAllocatedUnfairLock<[PluginInfo]>(initialState: [])
func ensureLoadedOnce() async {
if isLoadedOnce { return }
if let existing = loadOnceTask {
await existing.value
return
}
let task = Task { [weak self] in
await self?.performInitialLoad()
}
loadOnceTask = task
await task.value
}
}
Source: ScriptPluginManager.swift.
Why native matters for developer trust
A local network debugger touches system proxy settings, Keychain trust, certificates, files, app traffic, device traffic, and often internal services. Native macOS APIs are not a style preference here. They are part of the operating model.
Native also improves the day-to-day feel of a tool developers leave open. Fast tables, keyboard behavior, windows, menu commands, dark mode, file dialogs, Keychain prompts, and system integration all matter when the product is part of a debugging loop.
The open-source model completes the trust story. Engineers can read how the app is structured, inspect the helper boundary, audit the certificate path, review storage and redaction behavior, and build the project with Xcode.
Persistence follows the same principle: keep the hot path local and bounded, then spill larger or longer-lived evidence into explicit storage. The session store keeps large bodies out of SQLite rows and references them from disk-backed paths.
actor SessionStore {
/// Bodies larger than 1 MB are stored as individual files on disk
/// rather than inline in SQLite.
func saveTransaction(_ transaction: HTTPTransaction) throws {
let requestHeaders = encodeHeaders(transaction.request.headers)
let (requestBody, requestBodyPath) =
processBody(transaction.request.body, existingPath: nil)
let insert = Self.transactions.insert(
or: .replace,
Self.txId <- transaction.id.uuidString,
Self.txUrl <- transaction.request.url.absoluteString,
Self.txRequestHeaders <- requestHeaders,
Self.txRequestBody <- requestBody,
Self.txRequestBodyPath <- requestBodyPath
)
}
}
Source: SessionStore.swift.
The takeaway for developer tools
Rockxy's architecture is shaped by the work it performs. SwiftUI gives the app modern state-driven surfaces. AppKit handles macOS controls where scale and precision matter. SwiftNIO provides the proxy pipeline. Actors isolate runtime state. A helper boundary protects privileged operations. Certificate, storage, rules, plugins, and redaction have their own reviewable areas.
That structure supports the product strategy: one local macOS workflow for capture, inspection, replay, rules, diff, MCP-assisted analysis, mobile traffic, AI-era traffic, and Web3/RPC direction - without turning Rockxy into a cloud dashboard or protocol-specific app.
Related technical series
Start with Local-First Network Evidence for the product strategy, then read Trust, TLS, and Redaction for the security model.
Frequently asked questions
Why build a network debugger as a native macOS app?
A native macOS app can integrate with system proxy settings, Keychain, certificate trust, high-volume AppKit tables, and Swift concurrency while keeping capture and inspection local.
Why does Rockxy use SwiftNIO?
A debugging proxy needs control over HTTP, CONNECT tunnels, TLS interception, channel handlers, backpressure, and upstream forwarding. SwiftNIO provides the low-level event-driven pipeline needed for that work.
Where does SwiftUI fit?
SwiftUI is used for product surfaces such as windows, inspectors, settings, setup flows, and state-driven UI. AppKit is used where macOS control and high-volume table performance are the better fit.
How does architecture support security?
Rockxy separates UI orchestration, proxy runtime, certificate management, storage, mutation rules, plugin execution, and privileged helper operations so boundaries can be reviewed and tested independently.