Native SDK: Desktop Apps Compiled Straight to Zig, No WebView Anywhere

July 17, 2026

|repo-review

by Florian Narr

Native SDK: Desktop Apps Compiled Straight to Zig, No WebView Anywhere

vercel-labs/native is a toolkit for building native desktop apps. You write views in a custom .native markup format and logic in a constrained subset of TypeScript, and the whole thing compiles to a single Zig binary that draws every pixel itself. No Electron, no WebView, no JS runtime shipped in the final binary.

What it does

You author an app as three files of truth: a .native view, a src/core.ts logic module (a Model, a Msg union, one update function), and a manifest. native build compiles the TypeScript to native code and links it against a Zig-implemented rendering engine that owns the window, the widgets, and the event loop directly on the OS.

Why I starred it

The pitch is specific enough to be checkable: "no browser, no WebView, no JS runtime in the binary." Most cross-platform desktop frameworks solve the expressive-UI-vs-native-performance tradeoff by shipping a browser (Electron) or reusing the OS WebView (Tauri). Native SDK's answer is to not ship a runtime at all — TypeScript gets compiled away at build time, and the thing that's actually running is Zig.

That's an unusual bet from Vercel, a company whose entire business is the web runtime. And the repo backs it up with numbers you can check yourself: zig build.zig.zon declares zero dependencies (the terminal emulator is deliberately excluded from the pin to keep unrelated apps from paying for it), and the README claims the scaffolded counter app "builds to a single binary a few megabytes small."

How it works

The core abstraction is the same one Elm popularized: Model, Msg, update. In examples/chatbot/src/core.ts, every SSE line from a streaming chat completion becomes a Msg, and update is the only place state changes — markup can bind and dispatch, never mutate. What's different from a web Elm app is where that loop runs: src/tooling/ts_core.zig compiles this TypeScript subset ("the app-core subset") to native code at build time, not to JS that ships in a bundle.

src/tooling/ts_core.zig:1 lays out the detection contract plainly:

//! Multi-file cores: src/core.ts stays the detection root AND the entry
//! module, but a core may split into modules under src/ (relative imports
//! with real .ts filenames) plus SDK library modules
//! ("@native-sdk/core/text"). The frontend walks that import graph
//! itself, so `native check` reports diagnostics with each module's own
//! path...

An app has exactly one core — src/core.ts for TypeScript or src/main.zig for Zig — and ts_core.zig's detectAt() treats having both as a build error with a teaching message rather than silently picking one. The tree is the truth; there's no config flag for which language you're in.

The part that made me stop was src/automation/layout_fingerprint.zig. Native SDK has two wire formats that must never silently drift — the session replay journal and the automation dropbox protocol — and instead of a hand-maintained version integer, it computes a Wyhash over a comptime-generated structural description of the Zig type itself:

.@"struct" => |info| {
    var out: []const u8 = "struct{";
    for (info.fields) |field| {
        out = out ++ field.name ++ ":" ++ describe(field.type) ++ ",";
    }
    return out ++ "}";
},

Field names, field order, enum values, array lengths — anything that changes the wire shape moves the fingerprint automatically, because it's derived from @typeInfo at compile time rather than bumped by hand. The comment explaining why is worth quoting directly: a plain version integer "carried no information ('same or different' is the entire question) while costing two recurring failures: parallel branches contending for the next integer, and forgettable bumps." That's a genuinely clever use of Zig's comptime reflection to eliminate an entire class of merge conflict.

The virtualized list in examples/feed/src/main.zig is the other piece worth reading. It renders a 100,000-row timeline with variable-height rows, and the comment at the top calls out the actual hard problem: "the MODEL keyed by post index — rows scroll away, state does not." The runtime resolves scroll offset into a ui.virtualWindow, and only the rows inside that window get built via ui.virtualList — the scrollbar spans the full estimated extent and self-corrects toward the true value as you scroll past unmeasured rows, described in-line as "the scrollbar should always tell the truth."

Using it

npm install -g @native-sdk/cli
native init my_app
cd my_app
native dev

The npm package (packages/native-sdk/bin/native.js) is a thin dispatcher, not the CLI itself:

// Dispatcher for the `native` CLI: finds the prebuilt binary for this
// platform and execs it. The binary ships in a per-platform package
// (@native-sdk/cli-<platform>, an optionalDependency of this package),
// so installs run no scripts and download exactly one binary.

It resolves @native-sdk/cli-darwin-arm64 or @native-sdk/cli-linux-x64-gnu (checking for musl explicitly) as an npm optional dependency, so npm install never runs a postinstall script or hits a CDN outside npm's own registry — a pattern more CLI tools distributed through npm should copy.

native check validates .native markup against the app's actual Model/Msg types and reports file:line:column diagnostics without a full build — useful in an editor loop. native automate record journals a running session and replay reproduces it headlessly, verified frame-by-frame against the same kind of layout fingerprint described above.

Rough edges

The project is explicitly pre-1.0 and says so in AGENTS.md: "APIs still move, and the toolkit is evolving quickly." git log shows two contributors on this fork of the history I pulled, and the pace is real — 50 commits in the last 30 days against a repo that's been public a comparatively short time, several of them release-prep commits (v0.9.2 through v0.9.5) that suggest the API surface is still settling week to week.

Platform support is uneven by design, not oversight: the README states macOS has "the deepest support," Linux runs the full showcase through a software renderer, Windows is exercised in CI including real input injection, and mobile is explicitly "experimental." If you're targeting mobile today, this isn't there yet.

It's also a genuinely large codebase for a pre-1.0 tool — build.zig alone is 328KB, templates.zig and manifest.zig are each over 180KB. That's not a criticism of the engineering, but it means "read the whole thing before contributing" isn't realistic; you're trusting the skill-data/ agent skills and AGENTS.md to onboard you into one corner of it at a time.

Bottom line

If you want a desktop app that starts and renders like a native binary — not a Chromium instance with your bundle inside it — and you're comfortable working against a TypeScript subset instead of full Node semantics, this is worth building a real app in before it hits 1.0. If you need the full npm ecosystem inside your app logic or you're targeting mobile as a first-class platform, wait.

vercel-labs/native on GitHub
vercel-labs/native