# Platform support and security

> Host capability matrix for macOS, Linux, Windows, and experimental mobile, plus bridge permission and capability security boundaries.

- 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

- `docs/src/app/security/layout.tsx`
- `skill-data/core/references/bridge-security-native-capabilities.md`
- `src/platform/macos/appkit_host.m`
- `src/platform/linux/gtk_host.h`
- `src/tooling/android.zig`
- `src/tooling/ios.zig`

---

---
title: "Platform support and security"
description: "Host capability matrix for macOS, Linux, Windows, and experimental mobile, plus bridge permission and capability security boundaries."
---

Native SDK runs desktop apps through platform hosts in `src/platform/` (AppKit on macOS, GTK on Linux, Win32/WebView2 on Windows) and treats iOS/Android as experimental host tiers built on the embed C ABI. Feature gaps return `error.UnsupportedService` or bridge `permission_denied` rather than silent no-ops. Apps declare `permissions` and `capabilities` in `app.zon`; WebView content is untrusted until bridge policy, origin allowlists, and navigation rules opt it into native power.

## Support matrix

macOS, Linux, and Windows host full desktop apps. Mobile is experimental: simulator/emulator paths are verified, but APIs and tooling may still change. iOS and Android canvas apps are toolkit-owned hosts generated from `app.zon` via `native dev --target ios|android` and `native package --target ios|android` — the app project carries zero host code. Multi-window scenes remain desktop-only.

| Surface | macOS | Windows | Linux | iOS (experimental) | Android (experimental) |
| --- | --- | --- | --- | --- | --- |
| Real OS windows | Full | Full | Full | Full-screen single window in toolkit UIKit host | Full-screen single window in toolkit Android host |
| Native rendering | Metal + OS scroll physics | Software renderer, GDI blit | Software renderer, cairo blit | Software renderer; host presents via Metal | Software renderer; host copies pixels into surface |
| Pointer, keyboard, IME | Full | IME mapped; hardware IME verification pending | Full (pointer, keyboard, scroll, IME, HiDPI) | Touch, system keyboard, IME via host | Touch, soft keyboard, IME via host |
| Menus | Native app + context menus | Native app menus; context as anchored canvas | Native app menus; context as anchored canvas | None | None |
| System tray | Full (`NSStatusItem`) | Full | `UnsupportedService` until portable status-notifier | None | None |
| Web content | System WebView default; optional CEF Chromium | System WebView (maturing); no engine for pure canvas apps | System WebView | System WebView only in embed/host shell workspace | System WebView only in embed/host shell workspace |
| Packaging | `.app`, DMG, generated icons | Directory artifact + file-type registration; installer later | Install tree + desktop entry/MIME; AppImage later | Generated Xcode project; archive-ready; signing manual | Generated host + debug APK; store signing manual |
| Code signing | Ad-hoc and identity + entitlements; notarization manual | None | None | None | None |
| Automation | Full | Full (desktop + CI emulation) | Full under Xvfb | File-based protocol inside app container (simulator) | File-based protocol in app files dir (emulator) |

Unsupported host operations reject explicitly. Use `Runtime.supports(...)` / `native-sdk.platform.supports` before branching UI.

## Desktop hosts

| Area | macOS system WebView | macOS Chromium | Linux system WebView | Linux Chromium | Windows system WebView |
| --- | --- | --- | --- | --- | --- |
| Main / child WebViews | Supported | Supported | Supported | Unsupported | Supported |
| Native views / control commands | Supported | Unsupported | Supported | Unsupported | Supported |
| App menus | Supported | Unsupported | Supported | Unsupported | Supported |
| Context menus | Native | Native | Anchored canvas fallback | Anchored canvas fallback | Anchored canvas fallback |
| Tray | Supported | Supported | Unsupported | Unsupported | Supported |
| Shortcuts, dialogs, clipboard, open URL, reveal path, notifications, recent docs | Supported | Supported (subset per host) | Supported | Unsupported | Supported |
| Credentials | Supported | Supported | When secret service present | Unsupported | Supported |
| File drops | Supported | Unsupported | Supported | Unsupported | Supported |
| Audio playback / streaming / spectrum | Supported | Unsupported | When GStreamer present | Unsupported | Supported (spectrum needs Win10 2004+) |

Chromium (`web_engine = "chromium"`) is implemented on macOS only. Linux/Windows Chromium host paths are placeholders; tooling rejects those targets instead of substituting an engine.

**Presentation path:** macOS uses Metal. Linux and Windows present the deterministic software renderer (`backend=software`); a manifest requesting another GPU backend falls back to software on those hosts.

**Window chrome:** `titlebar = "hidden_inset"` / `"hidden_inset_tall"`, `window-drag` regions, and `on_chrome` insets work on AppKit, GTK (CSD header bar), and Win32 (caption reclaim + DWM buttons). Mobile hosts answer `on_chrome` with safe-area insets.

## Experimental mobile

### Toolkit-owned hosts

| Target | Dev loop | Package output | Host identity |
| --- | --- | --- | --- |
| iOS | `native dev --target ios` → embed lib for simulator, UIKit host, `simctl` launch | `native package --target ios` → complete Xcode project + device-slice library | Deployment target `15.0`; bundle id maps `_` → `-` |
| Android | `native dev --target android` → embed lib `aarch64-linux-android`, assemble/install debug APK via `adb` | `native package --target android` → generated host project + debug APK | minSdk `30`, targetSdk `35`, ABI `arm64-v8a`; application id maps hyphens → underscores |

Both hosts drive the app over the embed C ABI (`src/embed/`), register platform audio and image decode services, and forward touch/keyboard/IME. Frames use the deterministic CPU reference renderer (mobile GPU rendering is a later phase). Markup hot reload does not reach simulator/device yet — edit, then re-run `native dev --target …`.

### Mobile transport security (host-generated)

| Platform | Policy |
| --- | --- |
| iOS | `NSAllowsLocalNetworking` only — localhost/local network cleartext for the dev media loop; HTTPS required on the public internet |
| Android | `network_security_config.xml`: cleartext denied by default; cleartext allowed only for `localhost`, `127.0.0.1`, and emulator host `10.0.2.2`. Manifest declares `INTERNET`; APK stays `debuggable` for automation `run-as` |

### Embed vs canvas shell

Embedding on iOS/Android (UIKit/`WKWebView` or Android views/`WebView` workspace) shares experimental status with the toolkit host tier. Dynamic generic native-view bridge mutation remains a desktop API surface until mobile runtime view hosts land. See [Embedded app](/embed).

## Runtime support queries

`PlatformFeature` names the host capability surface (`main_webview`, `child_webviews`, `native_views`, `menus`, `tray`, `shortcuts`, `dialogs`, `clipboard_text`, `clipboard_rich_data`, `open_url`, `reveal_path`, `notifications`, `recent_documents`, `credentials`, `file_drops`, `app_activation_events`, `gpu_surfaces`, `gpu_surface_scroll_drivers`, `context_menus`, `view_surface_adoption`, `audio_playback`, `audio_streaming`, `audio_spectrum`).

```zig
if (runtime.supports(.native_views)) {
    try runtime.createView(.{ .label = "sidebar", .kind = .sidebar });
}
```

```javascript
const hasTray = await window.zero.platform.supports("tray");
// snake_case or camelCase: native_views / nativeViews
```

`native-sdk.platform.supports` requires `js_window_api` or an explicit `builtin_bridge` entry plus origin/permission checks. Missing features still reject with `error.UnsupportedService` if called blindly.

```mermaid
flowchart TB
  subgraph app [App process]
    ZON["app.zon permissions capabilities bridge security"]
    RT["Runtime + bridge policy"]
    CANVAS["Native-rendered UI / effects"]
    WV["WebView content window.zero"]
  end
  subgraph hosts [Platform hosts]
    MAC["macos/appkit_host"]
    LIN["linux/gtk_host"]
    WIN["windows webview2_host"]
    MOB["ios uikit_host / android host experimental"]
  end
  ZON --> RT
  RT --> CANVAS
  RT --> WV
  RT --> MAC
  RT --> LIN
  RT --> WIN
  RT --> MOB
  WV -->|"invoke + origin"| RT
  RT -->|"set_security_policy allowed_origins external_links"| MAC
  RT -->|"set_security_policy"| LIN
  RT -->|"set_security_policy"| WIN
```

## Security model

### Permissions vs capabilities

Declare both in `app.zon`:

```zig
.permissions = .{ "command", "view", "dialog", "window", "filesystem" },
.capabilities = .{ "webview", "js_bridge", "native_views", "dialog", "filesystem" },
```

| Kind | Role |
| --- | --- |
| `capabilities` | Broad features the app uses (manifest/feature inventory: webview, bridge, menus, tray, packaging metadata, …) |
| `permissions` | Runtime grants checked before native bridge commands and OS services run |

**Known permissions** (manifest parser; unknown strings become custom grants): `window`, `command`, `view`, `dialog`, `filesystem`, `clipboard`, `credentials`, `network`, `notifications`, `camera`, `microphone`, `location`. Custom permissions use reverse-DNS names (for example `com.example.my-permission`). Prefer the smallest set that covers the app.

**Known capabilities** (invalid names fail validation): `native_module`, `webview`, `js_bridge`, `native_views`, `gpu_surfaces`, `menus`, `shortcuts`, `tray`, `filesystem`, `network`, `notifications`, `dialog`, `clipboard`, `credentials`, `open_url`, `reveal_path`, `recent_documents`, `file_drops`, `app_activation_events`, `file_associations`, `url_schemes`.

Runtime security constants in `src/security/root.zig` include `permission_window`, `permission_command`, `permission_view`, `permission_dialog`, `permission_filesystem`, `permission_clipboard`, `permission_network`, `permission_notifications`, and `permission_credentials`.

### Default security policy

| Field | Default |
| --- | --- |
| `permissions` | Empty — nothing granted |
| `navigation.allowed_origins` | `zero://app`, `zero://inline` |
| `navigation.external_links.action` | `deny` |
| `navigation.external_links.allowed_urls` | Empty |

Hosts receive navigation policy through platform entry points such as `native_sdk_appkit_set_security_policy` (and GTK/Windows peers): allowed origins default to `zero://app` / `zero://inline` when unset; external URL lists and action flags gate system-browser opens.

### Bridge commands (default-deny)

JavaScript reaches Zig through:

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

Dispatch path:

1. Parse JSON request  
2. Enforce message size limits  
3. Check origin + required permissions against policy  
4. Look up registered handler  
5. Run handler and return JSON response  

A command must be **registered in Zig** and **allowed by policy**. Policy shape:

```zig
.bridge = .{
    .commands = .{
        .{ .name = "native.ping", .origins = .{ "zero://app" } },
        .{
            .name = "native-sdk.window.create",
            .permissions = .{ "window" },
            .origins = .{ "zero://app" },
        },
    },
},
```

Prefer exact origins over `"*"`. Wildcard is only appropriate for local development or commands that expose no native state.

**Bridge size limits** (`src/bridge/root.zig`):

| Limit | Value |
| --- | --- |
| Request / response / handler result | 1 MiB each (`max_message_bytes`, `max_response_bytes`, `max_result_bytes`) |
| Request id | 64 bytes |
| Command name | 128 bytes |

### Builtin bridge and `js_window_api`

Builtins (`native-sdk.command.*`, `native-sdk.window.*`, `native-sdk.view.*`, `native-sdk.webview.*`, `native-sdk.platform.*`, `native-sdk.dialog.*`, `native-sdk.os.*`, `native-sdk.clipboard.*`, `native-sdk.credentials.*`) are gated separately via `RuntimeOptions.builtin_bridge`.

When `builtin_bridge.enabled` is false:

- Commands with a `js_permission` require `js_window_api = true`, an allowed origin, and the matching permission (legacy: a bare `window` grant still satisfies some command/view helpers).
- Dialog, OS capability, clipboard, and credential builtins stay **default-deny** until listed in an explicit `builtin_bridge` policy.

```zig
.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.openFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
    },
},
```

Additional rules:

- View and WebView commands may only target the **calling** window.
- WebView navigations must pass `security.navigation.allowed_origins`.
- Child WebViews get `window.zero` only when created with `bridge: true`.
- Label `main` is reserved for the startup WebView.
- `native-sdk.os.openUrl` also requires `external_links` allowlisting; a bridge grant alone is not enough.

### Navigation and external links

```zig
.security = .{
    .navigation = .{
        .allowed_origins = .{
            "zero://app",
            "zero://inline",
            "http://127.0.0.1:5173",
        },
        .external_links = .{
            .action = "open_system_browser",
            .allowed_urls = .{ "https://example.com/docs/*" },
        },
    },
},
```

| Rule | Behavior |
| --- | --- |
| Origin allowlist | Exact match or `"*"`; unknown main-frame navigations blocked |
| External links | Denied unless `action = open_system_browser` and URL matches exact entry or `https://…/*` / `http://…/*` prefix (scheme + host + path prefix required; bare host wildcards rejected) |
| Same gate | `runtime.openExternalUrl(...)` and `window.zero.os.openUrl(...)` |

### Bridge error codes

JavaScript rejections expose `error.code`:

| Code | Meaning |
| --- | --- |
| `invalid_request` | Malformed input, denied navigation URL, missing target, reserved/duplicate label |
| `unknown_command` | No registered handler |
| `permission_denied` | Origin or permission check failed |
| `handler_failed` | Handler returned an error |
| `payload_too_large` | Request exceeds size limit |
| `internal_error` | Unexpected runtime failure |

### CSP

The runtime does **not** enforce Content Security Policy. Set CSP in app HTML. Packaged assets should start strict (`default-src 'self'`); dev servers may extend `connect-src` only for the local origin/WebSocket. Keep production CSP separate from development CSP.

### Layer summary

| Layer | Default | Opt-in |
| --- | --- | --- |
| App bridge commands | Denied | Per-command policy with origin + permissions |
| Builtin helpers (command/window/view/webview/platform) | Denied unless `js_window_api` or explicit policy | Matching permission + allowed origins |
| Builtin dialogs / OS / clipboard / credentials | Denied | Explicit `builtin_bridge` command list |
| Navigation | Blocked | `allowed_origins` |
| External links | Denied | `open_system_browser` + URL allowlist |
| Permissions | None | Declared in `app.zon`, checked at runtime |
| Host feature | Present or `UnsupportedService` | Query via `supports` / `platform.supports` |
| CSP | Not enforced by SDK | HTML `<meta>` |

Defense in depth: a Zig-registered command still fails unless policy allows the origin and required permissions.

## Verification signals

| Check | Expected signal |
| --- | --- |
| Desktop host live | Showcase apps under real windows (`tools/linux-truth` under Xvfb; `tools/windows-truth` on Windows desktop); CI headless canvas smokes |
| iOS host | `native dev --target ios` on simulator; package output archives with `xcodebuild` |
| Android host | `native dev --target android` on emulator; package assembles debug APK; embed ABI cross-compile in CI |
| Bridge deny | `permission_denied` / `unknown_command` from `window.zero.invoke` |
| Feature gap | `error.UnsupportedService` or `supports(...) == false` |
| External URL | Denied unless action + allowlist match (including prefix rules that reject host-boundary tricks) |

## Related pages

<CardGroup>
  <Card title="Capabilities" href="/capabilities">
    Guarded OS services: notifications, clipboard, credentials, dialogs, file drops, and effect-channel boundaries.
  </Card>
  <Card title="Bridge and builtin commands" href="/bridge">
    Host-to-content bridge payloads, async dispatch, builtin command catalog, and permission wiring.
  </Card>
  <Card title="Web engines" href="/web-engines">
    System WebView vs CEF, engine flags, and when canvas UI coexists with web content.
  </Card>
  <Card title="Windows and surfaces" href="/windows-surfaces">
    Multi-window composition, OS chrome, GPU surfaces, and host presentation.
  </Card>
  <Card title="Embedded app" href="/embed">
    C embed ABI, mobile host shims, and experimental iOS/Android integration.
  </Card>
  <Card title="Code signing" href="/code-signing">
    macOS codesign, entitlements, and package pipeline signing constraints.
  </Card>
  <Card title="Packaging" href="/packaging">
    Release binaries, platform package layouts, and mobile generated projects.
  </Card>
  <Card title="Installation" href="/installation">
    Platform package layout, prerequisites, and success signals per host OS.
  </Card>
</CardGroup>
