Piazzi

A Zig framework for desktop apps with web frontends. Coming soon.

Write your UI in HTML, CSS, and whatever JS framework you like. Write your native logic in Zig. Get one binary per platform, cross-compiled from one machine.

$ zig build -Dtarget=aarch64-macos
$ zig build -Dtarget=x86_64-windows
$ zig build -Dtarget=x86_64-linux-gnu.2.35

The sky behind the name is Cygnus, plotted from the Yale Bright Star Catalogue. The orange dot is 61 Cygni, the star Piazzi caught moving. Its drift is exaggerated.

Piazzi takes Tauri’s architecture, the system webview with capability-scoped native access and your frontend toolchain left alone, and moves every guarantee it can from runtime to compile time.

The native side is zig build and nothing else. No Rust toolchain, no Node in the native build, and no JS runtime inside the app you ship. The framework is sized so one person can read all of it in an afternoon.

Declare your API once, in Zig.

Write your API as a Zig struct. At compile time Piazzi reads it and derives the JS client, the TypeScript declarations, and the dispatch table. There is no second definition to keep in sync, so nothing can drift.

A type JavaScript can’t hold exactly is a compile error at the declaration, and the message tells you the fix. Try it on updated_at.

src/main.zig, which you write
const Note = struct {
    id: u32,
    title: []const u8,
    body: []const u8,
    updated_at: i53,
};

const Api = struct {
    pub const Events = union(enum) {
        notes_changed: struct { id: u32 },
    };

    pub fn listNotes(ctx: *piazzi.Ctx) ![]const Note {
        return store.all(ctx.arena);
    }

    pub fn saveNote(ctx: *piazzi.Ctx, note: Note) !void {
        try store.put(note);
        ctx.emit(.{ .notes_changed = .{ .id = note.id } });
    }
};
web/src/piazzi/api.d.ts, which zig build writeszig build stops here, so no api.d.ts is written
// Generated by zig build. Do not edit.
export interface Note {
  id: number;
  title: string;
  body: string;
  updated_at: number;
}

export function listNotes(): Promise<Note[]>;
export function saveNote(note: Note): Promise<void>;

export type Events = {
  notes_changed: { id: number };
};
$ zig build
src/main.zig:5:17: error: i64 can’t be represented exactly in JS
    updated_at: i64,
                ^~~
note: use i53, or piazzi.Int64 to send it as a bigint

Capabilities you don’t grant aren’t in the binary.

Piazzi borrows Tauri’s capability model and moves enforcement to compile time. A native capability your app doesn’t grant isn’t denied at runtime. Its code is never compiled in, and nm can prove it.

Grants live next to your API, not in a separate config file. Switch a few on and off and watch the audit.

src/main.zig
const App = piazzi.App(.{
    .api = Api,
    .capabilities = .{
        // .shell is never granted, so none of its code exists
    },
});
What the built binary contains
$ zig build audit
compiled in      fs, dialog, opener
not compiled in  clipboard, notification, http, shell

$ nm zig-out/bin/notes | grep -cE 'piazzi\.(clipboard|notification|http|shell)'
0

Know what every webview can render before you ship.

Your UI runs in three engines: WebView2 on Windows, WKWebView on macOS, and WebKitGTK on Linux. CSS that works in one can break in another. zig build check reads your built frontend and flags what the oldest engine you target can’t render.

It works out that oldest engine from the minimum OS versions you declare. It warns by default and fails the build in strict mode. It ships first, as a standalone tool called piazzi-check that runs on any dist/ folder, including a Tauri app’s.

Illustrative output
$ zig build check
web/dist/assets/main-4f2a.css:812
  text-wrap: balance unsupported in WebKitGTK 2.44 (linux), 3 occurrences
web/dist/assets/index-9c1b.js:4471
  Object.groupBy unsupported in the WebKit shipped with macOS 13.0
web/dist/assets/index-9c1b.js:5102
  heuristic: .at() seen on an unknown receiver

2 definite, 1 heuristic. Build continues (pass -Dstrict-check to fail).
Windows
WebView2, which tracks current Chromium
macOS
WKWebView, as shipped with your minimum macOS
Linux
WebKitGTK, at the version you declare

Every desktop binary from one machine.

Zig is the compiler, the build system, the linker, and the cross-compiler, with a libc for every target. Once each platform’s SDK pieces are fetched, one host builds all three binaries, one command each.

Signing and notarizing for macOS still needs a Mac, and most installers build on the system they target. So there’s no CI matrix for compiling, and a small one for packaging and signing.

What each target needs
TargetZig bringsYou fetch once
aarch64-macosDarwin libc and the linkerHeaders and link stubs from an Apple SDK you supply
x86_64-windowsmingw-w64 headers and libcWebView2 SDK headers, fetched by a build step
x86_64-linux-gnu.2.35glibc at the version you pickA GTK and WebKitGTK sysroot, assembled by a script

No hidden caveats.

Some problems come with the system webview. Piazzi inherits them like every framework in this category, and lists them here so you don’t have to find them yourself.

Shared, and documented rather than fixed

  • WebKitGTK has graphics trouble on Linux: blank windows, resize flicker, and renderer failures on some NVIDIA and Wayland setups.
  • Linux machines need WebKitGTK. The .deb declares it, and the AppImage bundles it and grows to match.
  • Stale or locked-down Windows installs can lack the WebView2 Runtime, so the installer fetches it.
  • Unsigned Windows apps trip Defender. The guide covers signing and building reputation.

Not in version 1

  • Mobile. No iOS, no Android.
  • A bundled engine. The engine is an interface from the first commit, so one engine everywhere can come in version 2 without a rewrite.
  • A plugin ABI. Capability modules live in the tree for now.
  • An auto-updater. The hook ships in 1.0 and the updater follows in 1.1.
  • Remote content in any view that can call your API.

What ships, in order.

Every milestone ends with something you can run. There are no dates here on purpose: this is the order things arrive in, and a milestone counts once it has shipped.

  1. Now

    The design

    Written down in full. Nothing below has shipped yet.

  2. M0

    Two go/no-go spikes

    A macOS window with a WKWebView through objc_msgSend, and a Windows window with WebView2 through COM. If either fails, the plan changes.

  3. M1

    piazzi-check, on its own

    The compatibility linter ships before the framework, with its own README, tested against real apps built with other frameworks.

  4. M2

    Scheme, assets, one origin

    Apps load from piazzi://app in dev and release alike, and the secure-context checks become tests.

  5. M3

    IPC by hand

    Calls in both directions, promises in JS, errors carried across, and one API thread with a per-request arena.

  6. M4

    Comptime bindings

    The generated client and types replace the hand-written dispatch. The first build-time and binary-size measurements are taken here.

  7. M5

    Windows

    The WebView2 backend, and the engine and platform interfaces redesigned against a second real platform.

  8. M6

    Security model

    Compile-time capability gating, runtime path scoping, CSP injection, the origin lock, and zig build audit.

  9. M7

    Linux and packaging

    The WebKitGTK backend, zig build bundle, and cross-compilation verified in CI.

  10. M8

    Version 1.0

    Docs, the Notes example app, a compatibility table, a signing guide, and a comparison where every number was measured.

  11. M9

    A bundled engine

    One engine on every platform. Not yet scoped, and planned for version 2.

Why it’s called Piazzi

1 January 1801

On the first night of 1801, Giuseppe Piazzi, at his observatory in Palermo, found Ceres, the first asteroid ever discovered. He also noticed 61 Cygni drifting against the fixed stars, and called it the Flying Star.

He made his name by noticing what had moved. A framework built to catch drift before it ships should carry it.