# Bridge and builtin commands

> Host-to-content bridge payloads, responses, async dispatch, builtin commands, and permission boundaries for WebView apps.

- Repository: vercel-labs/native
- GitHub: https://github.com/vercel-labs/native
- Human docs: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a/llms-full.txt

## Source Files

- `src/runtime/builtin_bridge.zig`
- `src/runtime/bridge_payload.zig`
- `src/runtime/bridge_responses.zig`
- `src/runtime/async_bridge.zig`
- `skill-data/core/references/bridge-security-native-capabilities.md`

---

---
title: "Bridge and builtin commands"
description: "Host-to-content bridge payloads, responses, async dispatch, builtin commands, and permission boundaries for WebView apps."
---

The host-to-content bridge is the JSON request/response path from WebView JavaScript into the Zig runtime. `handleBridgeMessage` in the runtime flow first routes `native-sdk.*` prefixes through `RuntimeBuiltinBridge`, then async handlers in `bridge.Dispatcher.async_registry`, then sync handlers in `bridge.Dispatcher.registry`. Every path is default-deny: policy must allow the command for the message origin before a handler runs.

## Message path

```mermaid
sequenceDiagram
  participant JS as WebView JS
  participant Plat as platform.services
  participant Flow as handleBridgeMessage
  participant Builtin as RuntimeBuiltinBridge
  participant Disp as bridge.Dispatcher
  participant Slot as AsyncBridgeResponseSlot

  JS->>Plat: bridge message bytes
  Plat->>Flow: BridgeMessage
  alt native-sdk.* prefix
    Flow->>Builtin: handleBuiltinBridgeMessage
    Builtin-->>Plat: completeWebViewBridge(response)
  else async_registry hit
    Flow->>Disp: parse + policy.allows
    Disp->>Slot: reserveAsyncBridgeResponse
    Note over Slot: handler later calls AsyncResponder
    Slot-->>Plat: respondToBridge
  else sync registry
    Flow->>Disp: dispatch(raw, source)
    Disp-->>Plat: completeWebViewBridge(response)
  end
```

| Stage | Behavior |
| --- | --- |
| Parse | `bridge.parseRequest` requires `id`, `command`, optional `payload` (default `null`) |
| Builtin gate | Commands starting with `native-sdk.command.`, `.window.`, `.view.`, `.webview.`, `.platform.`, `.dialog.`, `.os.`, `.clipboard.`, or `.credentials.` |
| App handlers | Sync: `registry` + `HandlerFn`. Async: `async_registry` + `AsyncHandlerFn` |
| Complete | `platform.services.completeWebViewBridge(window_id, webview_label, response)`; automation may also `publishBridgeResponse` |

## Wire protocol

### Request

```json
{
  "id": "1",
  "command": "native.ping",
  "payload": { "source": "webview" }
}
```

| Field | Rules |
| --- | --- |
| `id` | Required; 1–64 bytes; no control chars, `"` or `\` |
| `command` | Required; 1–128 bytes; no control chars, `"`, `\`, `/`, or spaces |
| `payload` | Optional JSON value; omitted → `null` |

### Success response

```json
{
  "id": "1",
  "ok": true,
  "result": { "message": "pong", "count": 1 }
}
```

Handler results must be valid JSON values. Empty results become `null`. Invalid JSON from a handler becomes `handler_failed`.

### Error response

```json
{
  "id": "1",
  "ok": false,
  "error": {
    "code": "permission_denied",
    "message": "Bridge command is not permitted"
  }
}
```

### Limits

| Constant | Value |
| --- | --- |
| `max_message_bytes` | 1 MiB |
| `max_response_bytes` | 1 MiB |
| `max_result_bytes` | 1 MiB |
| `max_id_bytes` | 64 |
| `max_command_bytes` | 128 |
| `max_async_bridge_responses` | 64 concurrent async slots |
| `max_bridge_origin_bytes` | 512 (async source origin copy) |

Oversized raw messages return `payload_too_large`. Async slot exhaustion returns `internal_error` with `AsyncBridgeResponseLimitReached`.

## Invocation source

Each handler receives `bridge.Invocation`:

| Field | Meaning |
| --- | --- |
| `request.id` / `request.command` / `request.payload` | Parsed request |
| `source.origin` | Page origin (for example `zero://app`) |
| `source.window_id` | Calling window (default `1`) |
| `source.webview_label` | Calling WebView label (default `"main"`) |

Async handlers get a stable copy of origin and webview label in an `AsyncBridgeResponseSlot`; the label remains valid after the platform message buffer is reused.

## App-defined handlers

### Sync

```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});
}

// Dispatcher:
// .policy = .{ .enabled = true, .commands = &policies },
// .registry = .{ .handlers = &self.handlers },
```

Escape user-controlled strings with `bridge.writeJsonStringValue` so the response stays valid JSON.

### Async

Register on `async_registry`. Policy is checked before the handler runs. Respond exactly once via `AsyncResponder`:

| Method | Effect |
| --- | --- |
| `success(id, result_json)` | Writes success envelope and delivers via `respondToBridge` |
| `fail(id, code, message)` | Writes error envelope |
| `respond(raw_response)` | Delivers a fully formed response buffer |

A second `respond` after release returns `AsyncBridgeResponseAlreadyCompleted`.

### Policy

```zig
.bridge = .{
    .policy = .{
        .enabled = true,
        .permissions = &.{ /* runtime grants mirrored when set */ },
        .commands = &.{
            .{ .name = "native.ping", .origins = &.{ "zero://app" } },
            .{ .name = "native.secure", .permissions = &.{ "filesystem" }, .origins = &.{ "zero://app" } },
        },
    },
    .registry = .{ .handlers = &handlers },
    .async_registry = .{ .handlers = &async_handlers },
},
```

`Policy.allows` requires `enabled`, a matching command name, all listed permissions present in policy grants, and an origin match (exact or `"*"`). Empty `origins` means any origin passes the origin check (permissions still apply). Prefer exact origins over `"*"`.

## Builtin command catalog

Builtin dispatch lives in `RuntimeBuiltinBridge`. Prefix matching is exact namespace, then command string equality.

### Command routing

| Command | JS permission (fallback path) | Role |
| --- | --- | --- |
| `native-sdk.command.invoke` | `command` | Dispatches `CommandEvent` with `source = .bridge` |
| `native-sdk.command.list` | `command` | Lists `options.commands` as `{id,title,enabled,checked}` |

Invoke payload accepts `name` or `id`. Response includes `name`, `source`, `windowId`, `viewLabel`, `trayItemId`. The view label is empty when the caller is `main`.

### Platform

| Command | JS permission | Role |
| --- | --- | --- |
| `native-sdk.platform.supports` | `window` | Feature probe via `PlatformFeature` (`feature` or `name`; snake_case or camelCase aliases) |

### Windows

| Command | JS permission | Role |
| --- | --- | --- |
| `native-sdk.window.list` | `window` | List open windows |
| `native-sdk.window.create` | `window` | Create window (`label`, `title`, `width`/`height` default 720×480, `x`/`y`, `url`, `restoreState`) |
| `native-sdk.window.focus` | `window` | Focus by selector |
| `native-sdk.window.close` | `window` | Close by selector |

Window JSON: `id`, `label`, `title`, `open`, `focused`, `x`, `y`, `width`, `height`, `scale`.

### Views

| Command | JS permission | Role |
| --- | --- | --- |
| `native-sdk.view.create` | `view` | Create native/GPU/webview-backed view |
| `native-sdk.view.list` | `view` | List views in calling window |
| `native-sdk.view.update` | `view` | Patch frame/layer/visibility/text/command/url/… |
| `native-sdk.view.setFrame` | `view` | Required frame update |
| `native-sdk.view.setVisible` | `view` | Show/hide |
| `native-sdk.view.focus` | `view` | Focus by label |
| `native-sdk.view.focusNext` / `focusPrevious` | `view` | Focus cycle |
| `native-sdk.view.close` | `view` | Close view |

`windowId` in view/webview payloads must match the calling window or the runtime returns cross-window denial (`CrossWindowViewDenied` / `CrossWindowWebViewDenied` → `invalid_request`).

### WebViews

| Command | JS permission | Role |
| --- | --- | --- |
| `native-sdk.webview.create` | `window` | Child WebView; `main` label reserved |
| `native-sdk.webview.list` | `window` | List in calling window |
| `native-sdk.webview.setFrame` | `window` | Resize/move |
| `native-sdk.webview.navigate` | `window` | Navigate (not main); URL must pass navigation policy |
| `native-sdk.webview.setZoom` | `window` | Zoom |
| `native-sdk.webview.setLayer` | `window` | Stack order |
| `native-sdk.webview.close` | `window` | Close child WebView |

Create defaults: `transparent: false`, `bridge: false`, `layer: 0`. Frame requires positive width/height. Child WebViews get `window.zero` only when `bridge: true`. Navigate and create validate URL origins via navigation policy (`NavigationDenied` → `invalid_request`).

WebView JSON: `label`, `windowId`, `url`, frame fields, `layer`, `zoom`, `transparent`, `bridge`, `focused`, `open`.

### Dialogs, OS, clipboard, credentials

These families always need an explicit `builtin_bridge` policy entry. They do **not** use the `js_window_api` fallback (no JS permission is attached).

| Family | Commands |
| --- | --- |
| Dialog | `native-sdk.dialog.openFile`, `.saveFile`, `.showMessage` |
| OS | `native-sdk.os.openUrl`, `.showNotification`, `.revealPath`, `.addRecentDocument`, `.clearRecentDocuments` |
| Clipboard | `native-sdk.clipboard.readText`, `.writeText`, `.read`, `.write` |
| Credentials | `native-sdk.credentials.set`, `.get`, `.delete` |

Typical permission tags in policy: `dialog`, `network`, `notifications`, `filesystem`, `clipboard`, `credentials` (see `security.permission_*`).

## Permission boundaries

### Two allow paths for builtins

`allowsBuiltinBridgeCommand`:

1. If `options.builtin_bridge.enabled` is true → only `builtin_bridge.allows(command, origin)` (command listed, permissions, origins).
2. Else, for command/window/view/webview/platform only:
   - require non-null JS permission,
   - require `js_window_api = true`,
   - require origin in `security.navigation.allowed_origins`,
   - if runtime permissions are non-empty, require the JS permission **or** (for non-`window` permissions) the legacy `window` grant.

Dialog / OS / clipboard / credentials skip path 2 and deny unless path 1 allows them.

### Security grants

| Constant | String |
| --- | --- |
| `permission_window` | `"window"` |
| `permission_command` | `"command"` |
| `permission_view` | `"view"` |
| `permission_dialog` | `"dialog"` |
| `permission_filesystem` | `"filesystem"` |
| `permission_clipboard` | `"clipboard"` |
| `permission_network` | `"network"` |
| `permission_notifications` | `"notifications"` |
| `permission_credentials` | `"credentials"` |

Default navigation allowlist: `zero://app`, `zero://inline`. External link default action is `deny`.

### Example: explicit builtin policy

```zig
const app_permissions = [_][]const u8{
    native_sdk.security.permission_window,
    native_sdk.security.permission_dialog,
};

.security = .{
    .permissions = &app_permissions,
    .navigation = .{ .allowed_origins = &.{ "zero://app" } },
},
.js_window_api = true,
.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "native-sdk.window.create", .permissions = &.{ "window" }, .origins = &.{ "zero://app" } },
        .{ .name = "native-sdk.webview.create", .permissions = &.{ "window" }, .origins = &.{ "zero://app" } },
        .{ .name = "native-sdk.dialog.openFile", .permissions = &.{ "dialog" }, .origins = &.{ "zero://app" } },
    },
},
```

`<Warning>`
`js_window_api = true` exposes convenience APIs such as `window.zero.windows.*` / `webviews.*` / `commands.*` / `views.*` / `platform.supports`. It does not bypass origin or permission checks.
`</Warning>`

## JavaScript surface

```javascript
// App command
const result = await window.zero.invoke("native.ping", { source: "webview" });

// Builtins (when allowed)
await window.zero.invoke("native-sdk.window.create", {
  label: "tools",
  title: "Tools",
  width: 420,
  height: 320,
});

await window.zero.invoke("native-sdk.webview.create", {
  label: "preview",
  url: "https://example.com",
  frame: { x: 24, y: 24, width: 480, height: 320 },
  layer: 10,
  bridge: false,
});

try {
  await window.zero.invoke("native.save", payload);
} catch (error) {
  console.error(error.code, error.message);
}
```

## Error codes

| Code | Typical cause |
| --- | --- |
| `invalid_request` | Malformed JSON; validation/limit/duplicate/reserved label; navigation denied; many builtin option errors |
| `unknown_command` | No app handler / unmatched builtin name within a family |
| `permission_denied` | Policy, origin, or missing permission (`Bridge command is not permitted`, or family-specific messages such as `Dialog API is not permitted`) |
| `handler_failed` | App handler error name, or non-JSON result |
| `payload_too_large` | Request exceeds `max_message_bytes` |
| `internal_error` | Unexpected runtime failure, async slot/origin copy failures, or unmapped builtin errors |

Builtin Zig errors map through `builtinBridgeErrorCode` / `builtinBridgeErrorMessage` (for example `WebView label "main" is reserved…`, `WebView windowId must match the calling window`).

## Constraints and failure modes

| Constraint | Failure signal |
| --- | --- |
| Default deny for bridge and builtins | `permission_denied` |
| Cross-window `windowId` on view/webview ops | `invalid_request` / cross-window message |
| Child WebView label `main` | `ReservedWebViewLabel` |
| URL not in navigation policy | `NavigationDenied` |
| Backend missing child WebView / focus / GPU kind | `invalid_request` with unsupported-backend message |
| Async respond twice | `AsyncBridgeResponseAlreadyCompleted` |
| More than 64 in-flight async responses | `AsyncBridgeResponseLimitReached` |

For large data, avoid stuffing entire payloads through one bridge result; use files, native resources, or chunked app protocols.

## Related pages

<CardGroup>
  <Card title="Web engines" href="/web-engines">
    WebView composition, CEF hosts, and when engine-drawn UI coexists with web content.
  </Card>
  <Card title="Capabilities" href="/capabilities">
    Guarded OS services (dialogs, clipboard, credentials, notifications) and effect-channel boundaries.
  </Card>
  <Card title="Commands and keyboard shortcuts" href="/commands-shortcuts">
    Single command routing across toolbar, menu, tray, shortcut, and bridge entry points.
  </Card>
  <Card title="Platform support and security" href="/platform-security">
    Host capability matrix plus bridge permission and security defaults.
  </Card>
  <Card title="Frontend projects" href="/frontend">
    Managed web shells and `native dev` integration that load content into the bridge host.
  </Card>
  <Card title="Windows and surfaces" href="/windows-surfaces">
    Multi-window composition and surfaces that builtin window/view/webview commands manipulate.
  </Card>
</CardGroup>
