How to Add and Secure Bridge Commands Between Zig and TypeScript in the Native SDK
You can expose Zig functionality to TypeScript by declaring bridge commands in app.zon, implementing the handler in Zig, and registering it with native_sdk.BridgeDispatcher, while the SDK enforces security through declared permissions and origin whitelists.
The vercel-labs/native repository provides a Native SDK that enables Zig-based applications to communicate with JavaScript/TypeScript layers through a strictly controlled bridge. Understanding how to properly add and secure bridge commands between Zig and TypeScript ensures your native capabilities remain sandboxed while providing rich functionality to the frontend.
Understanding the Bridge Architecture
The bridge system consists of several tightly integrated components that handle everything from manifest parsing to runtime security enforcement.
src/tooling/manifest.zig parses the bridge_commands array from your app.zon and creates BridgeCommandMetadata objects containing the command name, required permissions, and allowed origins.
src/tooling/templates.zig generates TypeScript stubs for each declared command, wiring them to the native_sdk.BridgeDispatcher runtime.
BridgeDispatcher (native_sdk.BridgeDispatcher) receives JSON payloads from the JavaScript side, validates security metadata, and routes requests to the corresponding Zig handlers.
BridgePolicy (builtin_bridge) determines which bridge capabilities are enabled (e.g., js_bridge) and can be customized per application.
Security Metadata includes permissions (native capabilities like filesystem) and origins (whitelisted URLs) declared per-command in app.zon.
When invoked, the flow follows this sequence: the TypeScript stub sends a JSON request over the bridge, the runtime validates the caller's origin and permissions, dispatches to the Zig handler, and returns the JSON response. Any security check failure results in a clear error that prevents privilege escalation.
Declaring Bridge Commands in app.zon
Begin by defining your command in the application manifest. The bridge.commands array accepts objects containing the command name, required permissions, and authorized origins.
// app.zon
bridge = {
commands = [
{
name = "native.ping"
permissions = ["filesystem"]
origins = ["zero://app"]
}
]
}
The name field uses dot notation to organize commands namespaces. The permissions array lists native capabilities the command requires, while origins restricts which URLs can invoke the command.
Implementing Zig Handlers
Create a Zig function that matches the expected bridge handler signature. The function receives an allocator for response memory and a JSON payload from the TypeScript caller.
// src/bridge.zig
const std = @import("std");
const native_sdk = @import("native_sdk");
pub fn native_ping(
allocator: *std.mem.Allocator,
payload: []const u8,
) ![]const u8 {
// Example: simply echo the payload back as JSON
const response = try std.json.stringifyAlloc(
allocator,
.{ .message = "pong", .payload = payload },
.{},
);
return response;
}
Bridge handlers must return a JSON-formatted byte slice allocated with the provided allocator, which the SDK frees after sending the response to JavaScript.
Registering Commands with BridgeDispatcher
Initialize the dispatcher and register your handler to make it available to the TypeScript runtime. This typically occurs during application startup.
// src/main.zig
const dispatcher = native_sdk.BridgeDispatcher.init(allocator);
try dispatcher.registerCommand("native.ping", native_ping);
The registerCommand method in src/tooling/manifest.zig consumes the parsed metadata from app.zon and maps the command string to your Zig function pointer.
Calling Commands from TypeScript
Once registered, call the bridge command through the auto-generated TypeScript stubs. The stubs handle JSON serialization and communication with the Zig runtime.
import { native } from "./bridge";
async function demo() {
const result = await native.ping({ hello: "world" });
console.log(result); // → { message: "pong", payload: {...} }
}
demo();
The bridge.ts module exports type-safe wrappers that correspond directly to your Zig implementations, with the native.ping function internally sending JSON RPC messages to the dispatcher.
Securing Bridge Commands with Permissions and Origins
Security enforcement happens automatically at runtime based on your app.zon declarations. The SDK validates both the calling origin and command permissions before executing the handler.
| Field | Purpose | Example Values |
|---|---|---|
| permissions | Native capabilities the command may access | "filesystem", "zero://app" |
| origins | Allowed caller URLs | "zero://app", "https://myapp.com" |
The runtime performs strict validation before dispatching:
if (!self.isOriginAllowed(request.origin, command.origins)) return error.ForbiddenOrigin;
if (!self.hasPermissions(request.caller, command.permissions)) return error.InsufficientPermissions;
To maintain security:
- Apply the least privilege principle by listing only required permissions
- Specify exact origins rather than wildcards to prevent cross-site invocation
- Validate input in your Zig handlers even after permission checks pass
- Run
zig build testto execute the SDK's test suite (tests/ts-core/*_e2e_tests.zig), which verifies that permissions and origins match declared metadata
Summary
- Declare bridge commands in
app.zonwith specificpermissionsandoriginsto establish the security boundary - Implement handlers using the standard Zig signature accepting an allocator and JSON payload, returning allocated JSON responses
- Register handlers using
BridgeDispatcher.registerCommand()to wire them to the TypeScript runtime - Access commands through auto-generated TypeScript stubs that handle the JSON RPC communication
- Validate origins and permissions at runtime to prevent privilege escalation and unauthorized native capability access
Frequently Asked Questions
How does the Native SDK prevent unauthorized bridge access?
The SDK enforces security at the dispatcher level through origin validation and permission checks defined in app.zon. Before executing any handler, the runtime verifies that the calling page's origin appears in the command's whitelist and that the command's declared permissions cover the requested native capabilities. Any mismatch returns a ForbiddenOrigin or InsufficientPermissions error.
What is the correct function signature for a Zig bridge handler?
Bridge handlers must accept a *std.mem.Allocator and a []const u8 JSON payload, returning ![]const u8. According to the source code in src/bridge.zig, the standard pattern is pub fn command_name(allocator: *std.mem.Allocator, payload: []const u8) ![]const u8, where the returned slice must be JSON-formatted and allocated with the provided allocator.
Where are the TypeScript bridge stubs generated?
The src/tooling/templates.zig file processes your app.zon declarations to generate TypeScript stubs in bridge.ts. These stubs export async functions (such as native.ping()) that serialize arguments to JSON and communicate with native_sdk.BridgeDispatcher, providing type-safe access to your Zig implementations.
How do I test bridge command security locally?
Run zig build test to execute the end-to-end test suite located in tests/ts-core/*_e2e_tests.zig. These tests verify that the manifest parsing correctly associates permissions and origins with commands, and that the dispatcher properly rejects requests from unauthorized origins or attempts to access undeclared capabilities.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →