sirhcosirhco.dev
case studyframeworkopen source

verve

Full-stack Zig web framework. Pure-Zig, zero deps, SSR with fine-grained reactivity, per-island WASM code splitting, single-binary deploy.

01 · problem

Problem

Most web frameworks force a trade between time-to-first-byte (SSR), interactive feel (client reactivity), and operational shape (one binary versus a Node/Bun/Deno toolchain). Pick two, live with the third. The Rust side of that trade buys correctness at the cost of compile times and macro syntax. The Node side buys ergonomics at the cost of a runtime in front of your runtime. I wanted a third option: server-rendered, reactive, and shipped as a single binary the way Go services already ship.

02 · shape

Shape

A pure-Zig framework with zero third-party dependencies. SSR happens through a *Node tree streamed to the socket via std.http.Server. The same Signal / Effect / Owner / Resource graph that drives server-side reactivity ships into a wasm32-freestanding client runtime — DOM updates are a consequence of Signal.set, not a parallel write path. Per-island WASM chunks share linear memory with the main client.wasm, so adding islands is roughly free. The whole thing — framework, scaffolder, server-fn codegen, island chunker — lives in one repo behind one zig fmt rule set.

03 · build

Build

build.zig does the heavy lifting at configure time. It walks app.Actions to emit typed server-fn client stubs, parses src/app/islands.zig to discover islands, compiles one WASM chunk per island, and embeds a public-asset manifest if -Dpublic-dir= is passed. Routes are comptime-parsed once into a []const Segment slice; the runtime matcher walks the slice without allocating. Suspense uses Renderer.streamRender to flush the shell first and drain parked boundaries as <template> + verveSwap(N) chunks. CSRF is HMAC-SHA256, auto-issued cookie + __csrf form field, SameSite=Strict. CSP nonce is a per-request 12-byte hex value stamped onto every <script> and <style> the renderer emits.

Islands ship as <verve-island data-name=… data-props=…> markers. The bridge fetches each chunk on first encounter, caches the instantiation, copies props through a shared scratch region, and calls hydrate(ptr, len, root_id). Chunks import their memory from the main client (env.memory), which drops per-chunk size to ~73 bytes versus ~180 bytes standalone.

verve — request pipeline A request enters std.http.Server, is matched by the comptime router, rendered through the Signal/Effect/Owner graph, streamed via Suspense, and any islands ship as per-chunk WASM that share linear memory with the main client. REQUEST std.http comptime router SSR — *Node tree streamed via std.Io.Writer CSRF · CSP nonce · gzip on-the-fly REACTIVE GRAPH — server + wasm Signal Effect Owner · cleanup Resource · Suspense client.wasm · shared memory island chunks ~73 B each · env.memory DOM Signal.set → diff SINGLE BINARY · ZERO DEPS · ZIG 0.16
figure · service topology
04 · result

Result

This site runs on Verve. The case study you are reading was rendered by std.http.Server, served by a 1.7 MB binary, and shipped without npm install, cargo build, or a separate frontend toolchain. The /verve route on this site demos the live features — islands, SSE, ActionForm + CSRF, Suspense — against the framework itself. The codebase ships with 18 topic docs, a showcase example exercising every public export, and prebuilt releases on four platforms. Releases live at github.com/sirhco/verve.

0
third-party dependencies
~73 B
per-island WASM chunk overhead
155+
tests across core, server, client, integration
4
release platforms (linux/macos × x86_64/aarch64)

stack

Zig 0.16std.http.Serverwasm32-freestandingno third-party deps