This document explains the internal architecture of PWAKit, including the bridge system, module registry, and message flow.
PWAKit uses a layered architecture:
┌─────────────────────────────────────────────────────────────┐
│ PWAKit App │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ SwiftUI App │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ ContentView │ │ LoadingView │ │ ErrorView │ │ │
│ │ └──────┬──────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────▼──────────────────────────────────────────┐ │ │
│ │ │ WebViewContainer │ │ │
│ │ │ ┌────────────────────────────────────────────┐ │ │ │
│ │ │ │ WKWebView │ │ │ │
│ │ │ └────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────┬──────────────────────┘ │ │
│ └────────────────────────────│─────────────────────────┘ │
│ │ │
│ ┌────────────────────────────▼────────────────────────┐ │
│ │ PWAKitCore │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ BridgeDispatcher │ │ │
│ │ └───────────────────────┬────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────────────▼───────────────────────┐ │ │
│ │ │ ModuleRegistry │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │Platform │ │Haptics │ │ Push │ ... │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
The BridgeDispatcher is the central routing hub implemented as a Swift actor for thread-safe message handling. It:
- Receives JSON messages from JavaScript
- Parses and validates message format
- Routes to the appropriate module
- Returns responses back to JavaScript
Location: kit/src/PWAKitCore/Bridge/BridgeDispatcher.swift
Thread-safe registry for looking up modules by name. Each module registers itself with a unique name (e.g., "haptics", "biometrics").
Location: kit/src/PWAKitCore/Bridge/ModuleRegistry.swift
All native modules implement this protocol:
public protocol PWAModule: Sendable {
static var moduleName: String { get }
static var supportedActions: [String] { get }
func handle(
action: String,
payload: AnyCodable?,
context: ModuleContext
) async throws -> AnyCodable?
}Location: kit/src/PWAKitCore/Modules/PWAModule.swift
Messages are sent via WebKit message handlers:
window.webkit.messageHandlers.pwakit.postMessage({
id: "uuid-string",
module: "haptics",
action: "impact",
payload: { style: "medium" },
});Responses are sent back via JavaScript evaluation:
// Success response
window.pwakit._handleResponse({
id: "uuid-string",
success: true,
data: { triggered: true },
});
// Error response
window.pwakit._handleResponse({
id: "uuid-string",
success: false,
error: "Unknown action",
});
// Event dispatch (unsolicited)
window.pwakit._handleEvent({
type: "push",
data: { title: "New Message", body: "..." },
});JavaScript Swift
│ │
│ bridge.call('mod', 'act') │
│ ─────────────────────────► │
│ │ BridgeDispatcher
│ │ └─► ModuleRegistry.get("mod")
│ │ └─► module.handle("act", payload, context)
│ │
│ { success: true, data } │
│ ◄───────────────────────── │
│ │
- JavaScript calls
bridge.call('moduleName', 'action', payload) - SDK sends JSON message via
webkit.messageHandlers.pwakit BridgeScriptMessageHandlerreceives the messageBridgeDispatcherparses and routes to the module- Module processes the action and returns
AnyCodable - Response is JSON-encoded and sent back via JavaScript evaluation
- SDK resolves the promise with the response data
pwa-config.json (source of truth)
│
▼
ConfigurationLoader (parses JSON)
│
▼
ConfigurationValidator (validates schema)
│
▼
ConfigurationStore (actor, immutable)
│
├─► Info.plist (synced via sync-config.sh)
└─► ModuleRegistration (feature flags)
At app startup, ModuleRegistration.registerDefaultModules():
- Checks feature flags from configuration
- Conditionally registers each module
- Only enabled modules are available to JavaScript
Disabled modules return "module not available" errors.
PWAKit uses Swift 6's strict concurrency model:
- Actors for shared mutable state (
BridgeDispatcher,ConfigurationStore) - Sendable constraint on all modules
- @MainActor for UIKit operations
- No race conditions by design
| Component | Location |
|---|---|
| App Entry | kit/src/PWAKit/App/PWAKitApp.swift |
| WebView Container | kit/src/PWAKit/Views/WebViewContainer.swift |
| Bridge Dispatcher | kit/src/PWAKitCore/Bridge/BridgeDispatcher.swift |
| Module Registry | kit/src/PWAKitCore/Bridge/ModuleRegistry.swift |
| Module Protocol | kit/src/PWAKitCore/Modules/PWAModule.swift |
| Module Registration | kit/src/PWAKitCore/Modules/ModuleRegistration.swift |
| Configuration Loader | kit/src/PWAKitCore/Configuration/ConfigurationLoader.swift |