Overview
GBGBridge solves a fundamental problem: web-based identity verification journeys need access to native device capabilities (camera, NFC, biometrics) that are not available — or are severely limited — in a browser context. Rather than building separate native UIs for every journey variation, GBGBridge lets a single web journey drive the user experience while delegating capability-intensive operations to the native host.Key Terminology
Architecture
Component Diagram
Layers
- Capability Layer — Typed slots (
CaptureCapability) and custom capabilities declare what the host supports.CameraDetectorprovides hardware and permission detection. - Routing Layer —
BridgeHostreceives messages from the WebView, decodes them, and routes requests to typed slot handlers, custom capability handlers, orBridgeCapabilityHandlerimplementations. - Handler Layer — Typed slots handle capture requests via closures and return strongly-typed
CaptureResultvalues. TheBridgeCapabilityHandlerprotocol supports arbitrary request handling. - Transport Layer —
BridgeWebViewConfiguratorandBridgeWebViewmanage the WebView, inject the bootstrap script, and wire up the message channels. - Observation Layer —
BridgeHostDelegatelets you observe all messages and handle unrouted requests.
Message Protocol
All communication uses structured JSON envelopes. The protocol version is1.0.
Message Envelope
Message Types
Message Flow
Correlation IDs
Every request has acorrelationId. When the host responds, it includes the same correlationId so the web journey can match responses to their original requests. Events generate their own unique correlation IDs.
Response Statuses
Request Routing
When a message arrives from the web journey:BridgeHostdecodes the JSON envelope into aBridgeMessage.- The message is appended to
receivedMessages(observable). - The delegate’s
bridgeHost(_:didReceive:)is called. - If the message type is
request:- If a handler is registered for the message’s
action, the handler’shandle(request:responder:)is called. - If no handler is registered, the message is added to
pendingRequestsand the delegate’sbridgeHost(_:unhandledRequest:)is called.
- If a handler is registered for the message’s
- If the message type is
responseorevent, no routing occurs — it is stored and reported to the delegate.
Typed Capability Slots
GBGBridge provides typed capability slots as the recommended way to declare and handle well-known capabilities. A typed slot is aCaptureCapability instance exposed as a property on BridgeHost.
Built-in Slots
Handler-as-Declaration
Setting ahandler on a typed slot simultaneously declares support and provides the implementation:
handler is set and isEnabled is true, the slot reports isSupported = true. This means the capability query response automatically reflects the current state — no separate configuration step is needed.
Automatic Features
Typed slots provide several features automatically:- Capability query integration — Supported slots appear in
capability.queryresponses with version and permission state metadata. - Result encoding — Handlers return strongly-typed
CaptureResultvalues. The SDK encodes them into the bridge protocol format (base64 image data, dimensions, etc.). - Busy rejection — If a request arrives while another is already active, the slot responds with an error automatically.
- SwiftUI reactivity —
activeRequestis@Published, so views can react to capture requests (e.g., present a camerafullScreenCover).
Permission State
Each typed slot has apermissionState property that can be populated using CameraDetector:
capability.query response, allowing the web journey to detect permission issues before attempting capture.
Capability Negotiation
Before attempting to use a native feature, the web journey sends acapability.query request. GBGBridge includes a built-in handler (CapabilityQueryHandler) that responds automatically.
When using init(hostVersion:), the response is built dynamically from typed slots and custom capabilities. When using init(configuration:), it uses the static BridgeConfiguration dictionary.
This pattern allows the web journey to:
- Adapt its UI — Show or hide steps based on available capabilities.
- Prevent errors — Avoid requesting capabilities that aren’t supported.
- Check permissions — Detect whether native permissions have been granted before attempting operations.
- Degrade gracefully — Fall back to alternative flows when capabilities are missing.
Query Response Format
permissionState field is included when the typed slot or configuration provides it. It is omitted for capabilities that don’t report permission state.
See the Capability Handling Guide for detailed patterns including typed slots, custom capabilities, and environment-specific behavior.
Transport Mechanism
Web → Native (Incoming)
The web journey calls:BridgeHost via the WKScriptMessageHandler protocol.
Native → Web (Outgoing)
The host evaluates JavaScript on the WebView:receive() function is established by the bootstrap script that BridgeWebViewConfigurator injects at document start.
Bootstrap Script
The default bootstrap script is:receive() function. The web journey replaces receive() with its own implementation once loaded. Because injection happens at document start, the namespace is guaranteed to exist before any web code runs.
You can supply a custom bootstrap script via BridgeConfiguration.bootstrapScript if your web journey requires additional initialization.
Threading Model
BridgeHostis annotated@MainActor. All property access and method calls must happen on the main thread.BridgeCapabilityHandler.handle(request:responder:)isasync, allowing handlers to perform asynchronous work (e.g., presenting a camera sheet, waiting for NFC scan completion).BridgeResponder.respond()is safe to call from any thread — it dispatches back to the main actor internally.BridgeWebViewis a SwiftUIUIViewRepresentableand follows standard SwiftUI threading rules.
Design Rationale
Why a message protocol instead of direct method calls?
A message protocol decouples the web journey from the native host. The web journey doesn’t need to know whether it’s running in an iOS app, an Android app, or an iframe — it sends the same messages regardless. This also allows the protocol to evolve without breaking existing integrations.Why declare capabilities upfront?
Capability declaration serves two purposes: it lets the web journey adapt before hitting a dead end, and it gives the host application explicit control over what features are exposed. A host app might have camera hardware but choose not to expose camera capture for a particular journey.Why typed capability slots?
Typed slots eliminate a class of integration errors. With the configuration-based approach, integrators had to keep capability declarations and handler registrations in sync manually — using matching string keys. Typed slots make this impossible to get wrong: setting a handler declares support in a single step. They also provide automatic result encoding, busy rejection, and permission state reporting.Why @MainActor on BridgeHost?
WebKit APIs (WKWebView, WKUserContentController) must be accessed on the main thread. Since BridgeHost interacts directly with WebKit for both receiving and sending messages, constraining it to the main actor eliminates a class of threading bugs.
Why no external dependencies?
GBGBridge is a foundational integration layer. Adding dependencies would increase binary size, create version conflicts with consumer apps, and introduce supply-chain risk. The framework uses only Apple system frameworks.Next Steps
- API Reference — Detailed documentation for every public symbol
- Embedding Guide — SwiftUI and UIKit integration patterns
- Messaging Guide — Sending events and handling requests