zero-native: the manifest that decides what your web app is allowed to do

Vercel Labs’ Zig framework treats `app.zon` as both security policy and build-time control plane, letting web UIs run inside a native shell without inheriting Electron’s usual bulk.

9 min read View on GitHub More from vercel-labs

A web app approaches a native OS checkpoint while a stamped manifest document stands at the center of the scene. Approved actions pass through a narrow gate into a Zig runtime and then into the operating system, while engine choice branches toward system WebView or CEF. It explains that zero-native makes the manifest the control surface for both permissions and packaging.
The framework’s main idea is that `app.zon` is not just config. It is the app’s policy layer and its shipping plan.
Key Takeaways

The manifest is the product

Most web-to-native frameworks hide the interesting part behind scaffolding. zero-native puts it in the open: `app.zon` is the place where the app declares what it is allowed to touch, which window it should open, and which engine it should use. That makes the manifest feel less like metadata and more like policy.

That matters because the security model becomes visible. Instead of assuming the frontend can do anything the bridge exposes, zero-native starts with the opposite assumption: the WebView is untrusted, and only explicitly granted capabilities can cross into the native layer.

A command only reaches the OS if the manifest and origin checks both pass. That is the framework’s security story in one flow.

A web UI gets a native passport

The bridge is simple on the surface and strict underneath. The frontend calls `window.zero.invoke(command, payload)`, which looks a lot like a JSON-RPC message: a named command, a payload, and an origin. The runtime then decides whether that message is allowed to travel any farther.

// Frontend
await window.zero.invoke("zero-native.window.create", {
  title: "Notes",
  width: 900,
  height: 700
});

await window.zero.invoke("zero-native.file.open", {
  filter: [".md", ".txt"]
});

await window.zero.invoke("zero-native.ping", {
  message: "hello"
});

The useful part is not that the bridge exists. It is that every message is mediated by the Zig runtime, which checks origin and manifest permissions before routing the command to a handler. If the manifest did not grant the `window` capability, the call stops there.

Zig is the glue, not the headline

Zig is the right kind of boring here. It gives the project direct C interop, a small runtime surface, and a build system that can orchestrate native code and frontend assets without layering on a second ecosystem just to speak to the OS.

Zig calls C directly, which keeps platform SDKs, native libraries, codecs, and local system integrations within reach when the WebView layer needs to do real native work.

That is why the shell can stay thin. The framework does not need a heavy compatibility layer to get to Cocoa, GTK, or Win32. It can reach those APIs directly, which is a big part of why the codebase feels like an application runtime instead of a desktop product welded onto a web toolchain.

A WSJ-style hedcut portrait of ctate, the maintainer associated with the project. The portrait uses fine stippling and hatching on a pure white background, and it is meant to identify the maintainer in the origin section rather than to dramatize the architecture.

One framework, two rendering strategies

zero-native is pragmatic about rendering. It can use the system WebView for the smallest possible footprint, or it can bundle Chromium through CEF when consistency matters more than size. That is a sharper tradeoff than many wrappers offer, because it lets the app decide what kind of shipping problem it has.

Dimensionzero-nativeTauriElectron
Native core languageZigRustJavaScript / Node.js
Web engineSystem WebView or CEFSystem WebViewBundled Chromium
Footprint goalSmall by default, optional consistencySmallConsistency first, bulk accepted
Bridge postureExplicit capabilities and origin checksPermissioned bridgeBroad runtime access with larger surface area
Best fitTeams that want thin shells and explicit policyTeams that want a light wrapperTeams that prioritize maturity and Chromium parity
A close-up of a build desk where a Zig build script reads app.zon like a control panel. One branch routes toward System WebView parts, another toward CEF parts, and unused components are boxed out of the final binary. It explains that compile-time manifest reading changes what code gets shipped.
The build script does not just compile. It decides which engine path survives into the binary.

How the build decides what ships

The build is part of the architecture, not a separate concern. `build.zig` reads the manifest at compile time, then uses that information to choose the right engine path and trim away code that is never needed in that configuration.

const manifest = web_engine_tool.readManifestConfig(b, .{
    .path = "app.zon",
});

if (manifest.web_engine == .system) {
    // Compile the system WebView path only.
} else {
    // Include CEF packaging and runtime bits.
}

That is the subtle win. The manifest is not a note for humans. It is a control plane that reaches all the way into the shipped artifact, which is why the framework feels unusually explicit for a web-first tool.

Why Vercel is here at all

Vercel’s interest makes sense if you read zero-native as a continuation of the web app lifecycle. A team already shipping React or Next.js does not always want to start over in Swift, C#, or Rust just to get a desktop shell. They want a path from web frontend to native distribution without swallowing Electron’s entire cost model.

zero-native is a Zig desktop app shell for modern web frontends. Use the platform WebView when you want the smallest possible app, or bundle Chromium through CEF when rendering consistency matters.

That positioning also explains the project’s tone. It is not trying to replace every native app framework. It is trying to give web teams a smaller, more legible shell with a security model they can actually inspect.

zero-native vs Electron and Tauri

The comparison is cleaner than the marketing noise around it. Electron buys universality with Chromium and Node.js bundled into every app. Tauri trims that footprint by leaning on the system WebView and Rust. zero-native tries to keep Tauri’s lightness while adding a second rendering path and a Zig core.

ProjectCore languageRenderer strategyMain tradeoff
zero-nativeZigSystem WebView or CEFExplicit policy and flexible engine choice
TauriRustSystem WebViewSmall footprint with a Rust-based core
ElectronJavaScript / Node.jsBundled ChromiumMaximum consistency at the cost of size

The important distinction is not who is fastest or smallest in every case. It is which assumptions are baked into the framework. Electron assumes you will carry Chromium everywhere. Tauri assumes the system WebView is enough. zero-native says that should be a manifest decision.

The bet

The bet behind zero-native is that desktop tooling will keep moving toward explicit boundaries. Not just smaller binaries, but clearer contracts between UI code, native capabilities, and the thing that actually gets shipped. That is a stronger argument than “Electron, but smaller.”

If that idea sticks, `app.zon` will be the memorable part. It turns the manifest into the place where policy, packaging, and runtime behavior meet.