Skip to main content
This tutorial walks you through building a complete web app that runs a GBG Go identity verification journey inside an iframe. By the end, you will have a working React app — backed by a small Node companion server — that mints a journey URL, embeds the journey via the Web Bridge SDK, handles document-capture requests from the journey, and returns the captured image back to the iframe. The app you build has two parts: a small Node/Express companion server that calls @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:
Add .env to .gitignore immediately — these credentials are never to be committed or shipped to the browser.

3. Write the server

Create server/index.ts:
Install cors:

4. Start the server

Add a script to package.json:
Then:
You should see Server listening on http://localhost:3000.

5. Test the server

Leave the server running. You’ll need it for the rest of the tutorial.

Create the React project

In a new terminal, from the gbg-web-tutorial directory:

Project structure

Replace src/App.tsx, and add a few new files. Target structure:

App entry point

Replace src/App.tsx:
This is a simple two-view router driven by state. For production, swap in your real router (React Router, TanStack Router, Next.js routing, etc.).

Configuration screen

Create src/SetupView.tsx:

Calling your backend

Create src/api.ts:
A single POST against the companion server’s /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. Create src/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:
  1. Declaring camera.document in the capabilities prop tells the journey “the host supports this” via capability.query responses.
  2. Registering the camera.document.capture action handler (in DocumentCaptureHandler, below) attaches the function that actually runs when the journey invokes the action.
Both are required. Declaring without registering means the journey thinks the host can fulfil the capability but no handler ever runs. Registering without declaring means the handler is wired up but the journey’s pre-check (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

Create src/DocumentCaptureHandler.tsx:
This is a development capture surface. For production, swap the <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 raw registerHandler / 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

  1. Make sure the companion server is running (npm run dev in server/).
  2. In a new terminal: npm run dev in client/. Vite starts at http://localhost:5173.
  3. Open http://localhost:5173. Leave the server URL as http://localhost:3000.
  4. Click Start Journey. The companion server mints a journey URL and the React app loads it in an iframe.
  5. When the journey reaches a document capture step, the capture surface appears as a full-screen overlay.
  6. 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 include cors({ 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. If bridge.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 needs allow="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.