Skip to content

Bridge Protocol v1

This page is the source of truth for the wire protocol between the UnrealReactBridge UE5 plugin and any web client. You do not need the unreal-react-bridge npm SDK — any web framework (Vue, Svelte, plain JS, htmx, …) can implement this protocol directly.

Plugin version1.x
Protocol version1
SDK version1.x

Transport mechanism

The plugin uses SWebBrowser::BindUObject (UE5 5.7+ official API) to expose a C++ object at window.ue.bridge in the browser context. UE5 automatically lowercases all method names, so OnReactEvent on the C++ side is reachable as window.ue.bridge.onreactevent in JavaScript.

Lifecycle

text
1. Developer calls LoadURL (or sets URL in WBP_WebHUD Details panel)
2. Page starts loading → UE calls BindUObject("bridge", component)
   → window.ue.bridge becomes available
3. Page finishes loading → UE injects init script:
   window.dispatchEvent(new CustomEvent('ue5-bridge-ready',
     { detail: { version: '1', initialState: { ... } } }))
4. Web side receives 'ue5-bridge-ready' → calls window.ue.bridge.onreactready()
5. UE OnReactReady() fires → flushes queued events
6. OnReactReadyEvent BP delegate broadcasts → bridge is live

On page reload or another LoadURL() call, the lifecycle restarts from step 1.

See /guide/lifecycle for a per-step walkthrough.

Web → UE

js
window.ue.bridge.onreactevent(eventName, jsonData)
ParamTypeDescription
eventNamestringColon-separated event name, e.g. "ui:reload:click"
jsonDatastringA valid JSON string, e.g. '{"weapon":"pistol","ammo":15}'

The plugin parses jsonData and forwards it to any UReactBridgeReceiverComponent bound to the matching event name, and to the Subsystem-level OnEventReceived delegate.

UE → Web

UE dispatches browser CustomEvents on window. The payload is always in event.detail as a parsed JavaScript object.

js
window.addEventListener('player:health:update', (event) => {
  const { value, max } = event.detail;
  updateHealthBar(value, max);
});

The payload is transmitted as base64-encoded UTF-8 JSON and decoded by the init script before dispatch. Your listener always receives a plain JS object — no manual decoding needed.

Signaling readiness

The web side must call onreactready() to start event delivery. Events sent by UE before readiness are queued (capacity governed by Max Event Queue Size — see /reference/settings) and flushed on receipt of the ready signal.

Cover both paths — bridge already present (page reload) and bridge arriving later (cold load):

js
// Cold-load path: listen for bridge init
window.addEventListener('ue5-bridge-ready', () => {
  window.ue.bridge.onreactready();
});

// Reload path: bridge already on window
if (window.ue?.bridge?.onreactready) {
  window.ue.bridge.onreactready();
}

Event naming convention

Recommended: domain:entity:action.

DirectionExampleMeaning
UE → Webplayer:health:updateUE sends updated health value
UE → Webgame:state:changeUE sends new game state
Web → UEui:reload:clickUser clicked reload button
Web → UEui:menu:openUser opened menu

Typed payload conventions

When a payload originates from UReactBridgeSenderComponent's typed Push* helpers (or the equivalent C++ template PushStruct<T>), the wire shape is always wrapped under value:

json
{ "value": <T> }
Send-side callevent.detail shape
PushFloat("player:health", 75.0f){ "value": 75 }
PushInt("player:score", 42){ "value": 42 }
PushString("player:name", "alice"){ "value": "alice" }
PushBool("ui:visible", true){ "value": true }
PushVector("player:loc", FVector(1,2,3)){ "value": { "X": 1, "Y": 2, "Z": 3 } }
PushStruct("player:state", FPlayerState{...}){ "value": <FJsonObjectConverter output> }
PushMap("inventory:slot", { {"id","sword"}, {"qty","1"} }){ "id": "sword", "qty": "1" } (flat, no envelope)
PushRaw("player:hp", "{\"value\":720,\"max\":900}"){ "value": 720, "max": 900 } (verbatim)

Two carve-outs:

  • PushMap emits the map as a flat object ({ "k1": "v1", ... }) — no value wrap (the map IS already an object). Values are all strings (the BP TMap<FString, FString> shape). Key order on the wire is non-deterministic — read by name, not by position.
  • PushRaw is a verbatim passthrough — no shape rule. Use it when you need numeric / nested / array payloads that bypass the envelope.

Subsystem-level BroadcastEvent / SendEvent and the BP-level WebHUD::Send API also pass the JSON payload through unmodified — the { "value": <X> } convention is specific to the typed sender helpers.

useUnrealState unwrap rule

useUnrealState<T>(name, initial) auto-unwraps the envelope only when the payload is an object with exactly one own key named value — i.e. the shape produced by the scalar helpers above. Payloads that pass through PushMap / PushRaw / Subsystem broadcast (objects with multiple keys, arrays, or primitives) reach the hook untouched, so e.g. { "value": 720, "max": 900 } is delivered as the full object rather than being silently collapsed to 720.

See /reference/react-hooks#useunrealstate.

Initial state

The ue5-bridge-ready event's detail always includes an initialState field — an object of arbitrary JSON-serializable key/value pairs the host UE side wants the web app to read on first paint.

js
window.addEventListener('ue5-bridge-ready', (event) => {
  const { version, initialState } = event.detail;
  console.log('player snapshot', initialState.player);
});

initialState is always present (empty object {} if the host provided nothing). v1 consumers that read only detail.version keep working unchanged.

Mapping rule on the UE side: each (key, jsonValue) pair in the host's InitialState map becomes key: <parsed JSON> if jsonValue is valid JSON; otherwise it becomes key: "<jsonValue>" (wrapped as string). BP authors can pass either structured JSON ({"hp":100}) or plain strings ("hello") — the latter just shows up as a JSON string on the web side.

See /guide/initial-state.

Layer registry note

The Add Web HUD Blueprint node creates UWebHUDLayer widgets that register with the same UUnrealReactBridgeSubsystem HUD-widget registry as legacy WBP_WebHUD widgets. The Name you pass to Add Web HUD becomes the BridgeName; Sender component routing, Subsystem SendEvent / BroadcastEvent, and the wire protocol itself behave identically for both kinds of widget. Mixing legacy widgets and new layers in the same project is supported.

Minimal vanilla JS implementation

html
<script>
  // Receive events from UE
  window.addEventListener('player:health:update', (e) => {
    document.getElementById('health').textContent = e.detail.value;
  });

  // Send events to UE
  function sendToUE(name, data) {
    if (window.ue?.bridge?.onreactevent) {
      window.ue.bridge.onreactevent(name, JSON.stringify(data));
    }
  }

  // Signal readiness (both paths)
  function signalReady() {
    if (window.ue?.bridge?.onreactready) window.ue.bridge.onreactready();
  }
  window.addEventListener('ue5-bridge-ready', signalReady);
  signalReady(); // in case bridge already exists (reload)
</script>

Version compatibility

Plugin VersionProtocol VersionSDK Version
1.x11.x

Breaking changes to the protocol increment the major version. The ue5-bridge-ready event detail includes { version: '1' } so the web side can detect mismatches.

See also

Released under the MIT License.