@gbg/go-core to mint journey sessions, and a React client that wraps the journey iframe in BridgeHostProvider and a capability handler. Total client code is around 150 lines.
Reference build: The gbg-go-web-reference repository is being staged under the gbgplc org and will host the complete source for this tutorial once GGO-14514 ships externally. Until then, follow this tutorial to build it step by step.
Prerequisites
You also need the Web Bridge SDK. See Getting Started → Installation.
Set up the backend
The browser does not call the GBG Go API directly. A companion server uses@gbg/go-core to mint journey sessions and returns the journey URL to the browser. This keeps API credentials server-side.
1. Create the server directory
2. Configure credentials
Create a.env file:
.env to .gitignore immediately — these credentials are never to be committed or shipped to the browser.
3. Write the server
Createserver/index.ts:
cors:
4. Start the server
Add a script topackage.json:
Server listening on http://localhost:3000.
5. Test the server
Create the React project
In a new terminal, from thegbg-web-tutorial directory:
Project structure
Replacesrc/App.tsx, and add a few new files. Target structure:
App entry point
Replacesrc/App.tsx:
Configuration screen
Createsrc/SetupView.tsx:
Calling your backend
Createsrc/api.ts:
/api/journey/start. The server authenticates with GBG Go, mints a journey instance, and returns the URL. The browser never touches @gbg/go-core credentials.
The bridge integration
This is the core of the integration. Createsrc/JourneyView.tsx:
How the bridge works
BridgeHostProvider is the React adapter around BridgeHost. It snapshots allowedOrigins, hostVersion, and capabilities when the iframe attaches, then instantiates the underlying host and exposes it to descendants through context.
The provider waits for iframe (a React state value) to transition from null to a real HTMLIFrameElement. That transition fires when the iframe element mounts and ref={setIframe} runs. Once attached, the bridge starts routing messages.
The two-step capability pattern
The web SDK splits declaration and registration:- Declaring
camera.documentin thecapabilitiesprop tells the journey “the host supports this” viacapability.queryresponses. - Registering the
camera.document.captureaction handler (inDocumentCaptureHandler, below) attaches the function that actually runs when the journey invokes the action.
hasCapability) returns false and it never invokes the action.
Permission state
navigator.permissions.query({ name: "camera" }) returns the current camera permission status. Passing it through CapabilityInfo.permissionState lets the journey prompt the user before triggering capture (e.g., “please grant camera access in browser settings”).
The permissionState snapshot is captured at provider attach time. To apply a live change, force a remount or use host.registerCustomCapability(...) for the affected capability.
Why useState, not useRef?
BridgeHostProvider watches iframe as part of its effect dependency array. Refs are not reactive — useRef never triggers a re-render, so the provider would never see the iframe attach. Always use useState<HTMLIFrameElement | null>(null) and pass setIframe as the ref callback.
Handling capture requests
Createsrc/DocumentCaptureHandler.tsx:
<input type="file"> for a real camera component — getUserMedia with a <video> preview, a third-party capture SDK, or your existing design-system camera component. See Capture Screens for richer patterns.
Why a ref for the responder?
BridgeResponder instances are not React state — they’re imperative handles for sending the response. Storing the active responder in a useRef lets the capture UI close over it without triggering re-renders on every message. When the capture finishes, the UI calls responderRef.current?.success(...) and clears the ref.
Why a busy guard?
The web SDK doesn’t enforce single-flight on rawregisterHandler / useBridgeCapability — that protection is part of iOS/Android’s typed slots. On web, do it yourself: if the responder ref is already set when a new request arrives, respond with a BUSY error.
Run the app
- Make sure the companion server is running (
npm run devinserver/). - In a new terminal:
npm run devinclient/. Vite starts athttp://localhost:5173. - Open
http://localhost:5173. Leave the server URL ashttp://localhost:3000. - Click Start Journey. The companion server mints a journey URL and the React app loads it in an iframe.
- When the journey reaches a document capture step, the capture surface appears as a full-screen overlay.
- Pick or capture a photo. The bridge sends the image back to the journey.
Common pitfalls
A handful of issues catch most teams running the tutorial app for the first time. The notes below cover the recurring ones.CORS errors
If the companion server doesn’t includecors({ origin: "http://localhost:5173" }), browser fetches fail with No 'Access-Control-Allow-Origin' header. The example server above includes CORS — match the client origin if you run Vite on a different port.
Mixed content
If your client is HTTPS (e.g., behind a tunnel for mobile testing) and the journey URL is HTTP, the iframe will fail silently. Always use HTTPS in production. For local dev,http://localhost works in both directions because browsers treat localhost as a secure context.
@gbg/go-core accidentally in the client bundle
If you import @gbg/go-core from any client-side file, your bundler may include it in the browser bundle — and the credentials with it. The Core SDK belongs in server/ only. Verify by searching your built bundle for @gbg/go-core after npm run build.
Capability query returns empty / handler never fires
The most common web integration mistake: forgetting either half of the two-step pattern. Ifbridge.hasCapability("camera.document") returns false in the journey, you forgot the capabilities prop. If the journey times out waiting for a response, you forgot to register the action handler.
Provider’s host stuck at null
useRef<HTMLIFrameElement>(null) does not trigger re-renders when the iframe element mounts. Use useState<HTMLIFrameElement | null>(null) and pass setIframe as the ref callback.
Camera permission not delegated to the iframe
The iframe needsallow="camera" to use getUserMedia inside its own context. Without it, even with host permission granted, the iframe’s calls fail.
What’s next
- Hello Journey — The smallest possible integration in one file.
- Two-Way Communication — Send events from host to iframe; register a custom capability; observe the message log.
- Advanced Integration — Pre-launch capability validation, lifecycle events, error states, and completion routing.
- Capture Screens — Production-grade capture surfaces with
getUserMedia, file pickers, and SDK swap points. - Concepts — Deeper dive into the architecture, message protocol, and capability system.