Hello world — desktop

Verve desktop apps are native executables that open an OS webview window (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux) and serve your UI over a custom verve://app/ scheme. The frontend talks to Zig through a typed IPC bridge — no HTTP server, no ports.

verve-cli new hello --desktop --template minimal
cd hello
zig build
./zig-out/bin/app

Type a name and click Greet — the button calls a Zig handler that formats the reply and sends it back over the bridge.

The entry point

src/main.zig opens the window, embeds the frontend assets, and wires the message handler:

zig
//! Minimal desktop scaffold entry point.
//!
//! Opens a window, wires the IPC handler, runs the platform event loop.
//! No tray, no multi-window, no smoke harness — see the full scaffold
//! (`verve-cli new <dir> --desktop`) for those.

const std = @import("std");
const desktop = @import("desktop");
const public_assets = @import("public_assets");
const handlers = @import("handlers.zig");

pub fn main(init: std.process.Init) !void {
    _ = init;

    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // The build embeds `frontend/` into `public_assets.entries` with
    // the same shape the desktop asset router consumes. Cast directly —
    // the field layout is intentionally compatible.
    const asset_entries: []const desktop.AssetEntry = @ptrCast(public_assets.entries);

    var window = try desktop.Window.init(allocator, .{
        .title = "Verve Desktop — minimal",
        .width = 480,
        .height = 320,
        .devtools = true,
        .assets = asset_entries,
        .initial_path = "index.html",
    });
    defer window.deinit();

    const ctx_ptr = handlers.attach(&window);
    window.setMessageHandler(handlers.onMessage, ctx_ptr);

    window.run();
}

Typed IPC routes

src/handlers.zig declares routes as a comptime struct table. Each public decl is one route: Args (decoded from the JS request), Reply (encoded back), and a handle function:

zig
//! Minimal IPC route table.
//!
//! Each route in `Routes` declares an `Args` type, a `Reply` type, and
//! a `handle(ctx, alloc, args)` fn. The router parses incoming JSON
//! against `Args`, calls the handler, JSON-encodes `Reply`, and ships
//! it back so `await window.verve.request(...)` resolves.

const std = @import("std");
const desktop = @import("desktop");

const RouterCtx = struct {
    window: *desktop.Window,
};

var ctx: RouterCtx = undefined;

const Router = desktop.Router(RouterCtx, Routes);

pub const onMessage = Router.dispatch;

pub fn attach(window: *desktop.Window) *RouterCtx {
    ctx = .{ .window = window };
    return &ctx;
}

const Routes = struct {
    pub const greet = struct {
        pub const Args = struct {
            name: []const u8 = "world",
        };
        pub const Reply = struct {
            message: []const u8,
        };
        pub fn handle(_: *RouterCtx, alloc: std.mem.Allocator, args: Args) !Reply {
            const trimmed = std.mem.trim(u8, args.name, &std.ascii.whitespace);
            const who = if (trimmed.len == 0) "world" else trimmed;
            const message = try std.fmt.allocPrint(alloc, "Hello, {s}!", .{who});
            return .{ .message = message };
        }
    };
};

Calling from the frontend

The bridge injects window.verve at document-start. The frontend is plain HTML/JS (the full desktop template adds SSR + WASM hydration):

const reply = await window.verve.request({ type: "greet", name: "Ada" });
// reply.message === "Hello, Ada!"

The type field selects the route; remaining fields decode into the route's Args struct. Type mismatches are rejected at the bridge, and unknown routes return an error reply.

Two desktop templates

--template minimal--desktop (full)
Window + IPCyesyes
Frontendstatic HTML/CSSSSR at build time + WASM hydration
Platform featurestray, notifications, deep links, cookies, print, multi-window
Dev loopzig buildzig build dev (watch + respawn), zig build smoke golden tests

Platform notes

  • macOS — links Cocoa + WebKit; zig build bundle produces a .app.
  • Windows — WebView2 header and loader DLL are vendored; needs the Evergreen runtime at run time (preinstalled on Windows 11).
  • Linux — GTK3 default (libgtk-3-dev libwebkit2gtk-4.1-dev), GTK4 via -Dgtk4=true (tray and window snapshot unsupported on GTK4).

The desktop guides cover the full platform API — windows, tray, notifications, hotkeys, deep links, file watching, and more. Start at Desktop architecture.