Bridge protocol
Most applications use generated APIs and never construct protocol messages directly. This reference explains what crosses the WebView boundary and how the host rejects incompatible contracts.
Message round-trip
sequenceDiagram
participant App as Web application
participant SDK as Generated SDK
participant Host as MAUI host
participant Contract as Native contract
App->>SDK: filesystem.readText({ path })
SDK->>Host: request
Host->>Contract: dispatch by contract and member
Contract-->>Host: typed result
Host-->>SDK: response
SDK-->>App: Promise result
Event contracts and JavaScript contracts use the same bridge in the opposite direction.
Transport
JavaScript sends messages through the first available channel:
- Native message channel
- Apple:
window.webkit.messageHandlers.vidra.postMessage(frame) - Windows:
window.chrome.webview.postMessage(frame)
- Apple:
- Custom-scheme fallback
vidra://bridgefor native requests.vidra://reversefor JavaScript-contract responses.
C# pushes messages through WebView JavaScript callbacks:
window.__vidra_callback(response)for native-contract responses.window.__vidra_onevent(event)for event contracts.window.__vidra_invoke(request)for JavaScript contracts.window.__vidra_initialize(handshake)for startup negotiation.
Startup negotiation
After navigation, the host sends:
{
"protocolVersion": 2,
"coreFingerprint": "sha256",
"appFingerprint": "sha256"
}
The SDK verifies the wire version and both generated manifest fingerprints before accepting traffic. The numeric wire version is an implementation compatibility field; there is only one supported public bridge protocol.
Native request
{
"id": "req_1",
"contract": "filesystem",
"member": "readText",
"payload": { "path": "/tmp/notes.txt" }
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Correlation identifier |
contract | string | yes | Target native contract |
member | string | yes | Native member |
payload | object | no | Typed member payload |
Response
{
"id": "req_1",
"success": true,
"data": { "content": "hello" }
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Matching correlation identifier |
success | boolean | yes | Whether the call succeeded |
data | any | no | Typed result on success |
error | object | no | { code, message } on failure |
Event
{
"contract": "connectivity",
"member": "changed",
"payload": { "access": "internet", "profiles": ["wifi"] }
}
Events have no correlation identifier or response.
JavaScript-contract request
{
"id": "js_1",
"contract": "counter",
"member": "increment"
}
JavaScript sends the result back in a response envelope. The bridge has a 30-second timeout; caller cancellation may finish sooner. Neither path stops a JavaScript handler that is already running.
Error codes
| Code | Meaning |
|---|---|
PARSE_ERROR | The envelope could not be deserialized |
NATIVE_CONTRACT_NOT_FOUND | No native contract has the requested name |
NATIVE_MEMBER_ERROR | A native member rejected or failed the request |
JS_HANDLER_NOT_FOUND | No JavaScript handler is registered |
JS_HANDLER_ERROR | A JavaScript handler threw or rejected |
JS_RESPONSE_INVALID | A JavaScript-contract response could not be decoded |
BROWSER_ONLY | A native contract was called outside the host |
Generated APIs are manifest-backed and fingerprinted. Dynamic calls through
vidra.unsafe and Bridge.Unsafe use the same envelopes but are not protected
by generated contracts.