---
title: Bridge
url: "https://native-sdk.dev/docs/bridge"
docs_index: /llms.txt
lastUpdated: 2026-10-13
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

For apps that [embed web content](/docs/frontend), the bridge connects JavaScript in the WebView to native Zig handlers via JSON messages. Native-rendered apps have no bridge — markup dispatches typed messages straight into `update` (see [App Model](/docs/app-model)).

## Architecture

```
WebView JS                       Zig Runtime
──────────                       ───────────
window.zero.invoke(cmd, payload)
        │                              │
        ├──── JSON message ───────────►│
        │                         Size check (16 KiB max)
        │                         Policy check (origin + permissions)
        │                         Handler lookup + execute
        │◄─── JSON response ──────────┤
```

## Defining a handler

```zig
fn ping(context: *anyopaque, invocation: native_sdk.bridge.Invocation, output: []u8) anyerror![]const u8 {
    _ = invocation;
    const self: *App = @ptrCast(@alignCast(context));
    self.ping_count += 1;
    return std.fmt.bufPrint(output, "{{\"message\":\"pong\",\"count\":{d}}}", .{self.ping_count});
}
```

The handler writes its JSON result into the provided `output` buffer (max 12 KiB) and returns a slice of it. Results must be valid JSON values; invalid raw text is rejected with `handler_failed`. When returning user data as a string, use the bridge helper so quotes and control characters are escaped:

```zig
return native_sdk.bridge.writeJsonStringValue(output, user_supplied_name);
```

## Wiring the dispatcher

```zig
fn bridge(self: *App) native_sdk.BridgeDispatcher {
    self.handlers = .{.{ .name = "native.ping", .context = self, .invoke_fn = ping }};
    return .{
        .policy = .{ .enabled = true, .commands = &policies },
        .registry = .{ .handlers = &self.handlers },
    };
}
```

## Calling from JavaScript

```javascript
const result = await window.zero.invoke("native.ping", { source: "webview" });
console.log(result); // { message: "pong from Zig", count: 1 }
```

## Invocation

When a handler is called, it receives an `Invocation` with:

- `request.id` -- caller-provided request ID (max 64 bytes)
- `request.command` -- command name (max 128 bytes, no `/` or spaces)
- `request.payload` -- JSON payload string
- `source.origin` -- origin of the requesting page (e.g. `zero://app`)
- `source.window_id` -- which window sent the request

## Size limits

<table>
  <thead>
    <tr>
      <th>
        Constant
      </th>

      <th>
        Value
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `max_message_bytes`
      </td>

      <td>
        16 KiB
      </td>
    </tr>

    <tr>
      <td>
        `max_response_bytes`
      </td>

      <td>
        16 KiB
      </td>
    </tr>

    <tr>
      <td>
        `max_result_bytes`
      </td>

      <td>
        12 KiB
      </td>
    </tr>

    <tr>
      <td>
        `max_id_bytes`
      </td>

      <td>
        64
      </td>
    </tr>

    <tr>
      <td>
        `max_command_bytes`
      </td>

      <td>
        128
      </td>
    </tr>
  </tbody>
</table>

## Error codes

When a bridge call fails, the JS promise rejects with an error containing a `code` field:

<table>
  <thead>
    <tr>
      <th>
        Code
      </th>

      <th>
        Cause
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `invalid_request`
      </td>

      <td>
        Malformed JSON message
      </td>
    </tr>

    <tr>
      <td>
        `unknown_command`
      </td>

      <td>
        No handler registered
      </td>
    </tr>

    <tr>
      <td>
        `permission_denied`
      </td>

      <td>
        Origin or permission check failed
      </td>
    </tr>

    <tr>
      <td>
        `handler_failed`
      </td>

      <td>
        Handler returned an error
      </td>
    </tr>

    <tr>
      <td>
        `payload_too_large`
      </td>

      <td>
        Message exceeds 16 KiB
      </td>
    </tr>

    <tr>
      <td>
        `internal_error`
      </td>

      <td>
        Unexpected runtime error
      </td>
    </tr>
  </tbody>
</table>

```javascript
try {
  const result = await window.zero.invoke("native.ping", {});
} catch (error) {
  console.error(error.code, error.message);
}
```

## Bridge types

<table>
  <thead>
    <tr>
      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `BridgeDispatcher`
      </td>

      <td>
        Combines policy and registry
      </td>
    </tr>

    <tr>
      <td>
        `BridgePolicy`
      </td>

      <td>
        Whether the bridge is enabled and which commands are allowed
      </td>
    </tr>

    <tr>
      <td>
        `BridgeCommandPolicy`
      </td>

      <td>
        Per-command:

        `name`

        ,

        `permissions`

        ,

        `origins`
      </td>
    </tr>

    <tr>
      <td>
        `BridgeRegistry`
      </td>

      <td>
        Maps command names to handler functions
      </td>
    </tr>

    <tr>
      <td>
        `BridgeHandler`
      </td>

      <td>
        `name`

        ,

        `context`

        ,

        `invoke_fn`
      </td>
    </tr>
  </tbody>
</table>

See also: [Builtin Commands](/docs/bridge/builtin-commands) for `native-sdk.command.*`, `native-sdk.window.*`, `native-sdk.view.*`, `native-sdk.webview.*`, `native-sdk.dialog.*`, `native-sdk.os.*`, `native-sdk.clipboard.*`, and `native-sdk.credentials.*`.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)