Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.go.gbgplc.com/llms.txt

Use this file to discover all available pages before exploring further.

This document explains the architecture and design principles behind GBGBridge. Understanding these concepts will help you integrate effectively and make informed decisions about how to structure your host application.

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

TermDefinition
JourneyA web-based identity verification flow (e.g., document capture + face match + NFC read). The journey runs as HTML/JavaScript inside a WebView.
HostThe native iOS application that embeds the journey WebView. The host provides native capabilities.
BridgeThe communication layer between the web journey and the native host. Comprises JavaScript stubs on the web side and BridgeHost on the native side.
CapabilityA native feature that the host can provide (e.g., camera.document, nfc.read). Capabilities can be declared via typed slots (recommended) or in configuration.
Typed SlotA built-in CaptureCapability property on BridgeHost (e.g., documentCapture, selfieCapture). Setting a handler on a slot declares the capability as supported.
HandlerA native Swift object conforming to BridgeCapabilityHandler that fulfills requests for a specific action, or a closure set on a typed slot.
Permission StateMetadata about the native permission status for a capability (e.g., granted, denied, notDetermined). Reported in capability query responses.
MessageA structured JSON envelope exchanged between the web journey and the native host. Every message has a type (request, response, or event), a correlation ID, and a payload.
ActionA string identifier for the operation a message relates to (e.g., "camera.document.capture", "capability.query").
ResponderA callback object provided to handlers, used to send a response back to the web journey.
Bootstrap ScriptA JavaScript snippet injected at document start that initializes the window.GBGBridge namespace and receive() function.

Architecture

Component Diagram

Layers

  1. Capability Layer — Typed slots (CaptureCapability) and custom capabilities declare what the host supports. CameraDetector provides hardware and permission detection.
  2. Routing Layer — BridgeHost receives messages from the WebView, decodes them, and routes requests to typed slot handlers, custom capability handlers, or BridgeCapabilityHandler implementations.
  3. Handler Layer — Typed slots handle capture requests via closures and return strongly-typed CaptureResult values. The BridgeCapabilityHandler protocol supports arbitrary request handling.
  4. Transport Layer — BridgeWebViewConfigurator and BridgeWebView manage the WebView, inject the bootstrap script, and wire up the message channels.
  5. Observation Layer — BridgeHostDelegate lets you observe all messages and handle unrouted requests.

Message Protocol

All communication uses structured JSON envelopes. The protocol version is 1.0.

Message Envelope

{
  "version": "1.0",
  "correlationId": "uuid-string",
  "type": "request | response | event",
  "timestamp": 1700000000000,
  "payload": {
    "action": "camera.document.capture",
    "data": { },
    "status": "success | error | cancelled | unsupported | acknowledged",
    "error": {
      "code": "CAMERA_DENIED",
      "message": "User denied camera access",
      "recoverable": true
    }
  }
}

Message Types

TypeDirectionPurpose
requestWeb → NativeThe web journey asks the host to perform an action.
responseNative → WebThe host sends the result of a request back to the web journey.
eventEither directionAn asynchronous notification with no expected response.

Message Flow

Correlation IDs

Every request has a correlationId. 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

StatusMeaning
successThe request completed successfully. Check data for the result.
errorThe request failed. Check error for details.
cancelledThe user cancelled the operation (e.g., dismissed a camera sheet).
unsupportedThe requested capability is not available on this host.
acknowledgedThe request was received and is being processed asynchronously.

Request Routing

When a message arrives from the web journey:
  1. BridgeHost decodes the JSON envelope into a BridgeMessage.
  2. The message is appended to receivedMessages (observable).
  3. The delegate’s bridgeHost(_:didReceive:) is called.
  4. If the message type is request:
    • If a handler is registered for the message’s action, the handler’s handle(request:responder:) is called.
    • If no handler is registered, the message is added to pendingRequests and the delegate’s bridgeHost(_:unhandledRequest:) is called.
  5. If the message type is response or event, 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 a CaptureCapability instance exposed as a property on BridgeHost.

Built-in Slots

SlotPropertyCapability IDAction ID
Document Capturehost.documentCapturecamera.documentcamera.document.capture
Selfie Capturehost.selfieCapturecamera.selfiecamera.selfie.capture

Handler-as-Declaration

Setting a handler on a typed slot simultaneously declares support and provides the implementation:
// This single line declares the capability AND provides the handler
host.documentCapture.handler = { request in
    return await host.documentCapture.awaitCompletion()
}
When 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.query responses with version and permission state metadata.
  • Result encoding — Handlers return strongly-typed CaptureResult values. 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 — activeRequest is @Published, so views can react to capture requests (e.g., present a camera fullScreenCover).

Permission State

Each typed slot has a permissionState property that can be populated using CameraDetector:
let camera = CameraDetector.check()
host.documentCapture.permissionState = camera.permissionState
host.selfieCapture.permissionState = camera.permissionState
This information is included in the 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 a capability.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

{
  "environment": "ios",
  "hostVersion": "1.0.0",
  "capabilities": {
    "camera.document": {
      "supported": true,
      "version": "1.0",
      "permissionState": "granted"
    },
    "camera.selfie": {
      "supported": true,
      "version": "1.0",
      "permissionState": "notDetermined"
    },
    "nfc.read": {
      "supported": true,
      "version": "1.0"
    }
  }
}
The 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:
window.webkit.messageHandlers.gbgBridge.postMessage(messageObject);
WebKit delivers this to BridgeHost via the WKScriptMessageHandler protocol.

Native → Web (Outgoing)

The host evaluates JavaScript on the WebView:
window.GBGBridge.receive(parsedMessageObject);
The receive() function is established by the bootstrap script that BridgeWebViewConfigurator injects at document start.

Bootstrap Script

The default bootstrap script is:
window.GBGBridge = window.GBGBridge || {};
window.GBGBridge.receive = window.GBGBridge.receive || function(){};
This creates the namespace and a no-op 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

  • BridgeHost is annotated @MainActor. All property access and method calls must happen on the main thread.
  • BridgeCapabilityHandler.handle(request:responder:) is async, 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.
  • BridgeWebView is a SwiftUI UIViewRepresentable and 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