Skip to content

srdjan/zigttp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

636 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zigttp

🌐 Web Site

A JavaScript runtime built from scratch in Zig for serverless workloads. One binary, no dependencies, instant cold starts.

Where Node.js and Deno optimize for generality, zigttp optimizes for a single use case: running a request handler as fast as possible, then getting out of the way. It ships a pure-Zig JS engine (zigts) with a JIT compiler, NaN-boxed values, and hidden classes - but skips everything a FaaS handler doesn't need (event loop, Promises, require).

Validated release target: Zig 0.16.0. The build produces three binaries: zigttp (the primary developer CLI and local runtime entrypoint), zigttp-runtime (the internal runtime template used for self-contained outputs), and zigts (the compiler/analyzer plus interactive coding-agent CLI).

What makes it different

Opinionated language subset. TypeScript with the footguns removed. No classes, no this, no var, no while loops, no switch - just functions, let/const, arrow functions, destructuring, for...of, match expressions, and assert statements. Unsupported features fail at parse time with a suggested alternative, not at runtime with a cryptic stack trace. The payoff: each restriction enables a compiler guarantee that would be impossible with general JavaScript.

JSX as a first-class primitive. The parser handles JSX directly - no Babel, no build step. Write TSX handlers that return server-rendered HTML.

Compile-time verification. -Dverify proves your handler correct at build time: every code path returns a Response, Result values are checked before access, no unreachable code. The IR tree is the control flow graph: no back-edges, no exceptions. break and continue within for-of are forward jumps only and don't compromise this property. See verification docs.

Sound mode. The compiler uses type inference to catch bugs across all operators at compile time, not just in boolean contexts. Arithmetic on non-numeric types is rejected ("hello" - 1, true * 5, env("X") / 2). Mixed-type + is an error - use template literals. Tautological comparisons (typeof x === "number" when x is provably number) emit warnings. In boolean contexts, types with unambiguous falsy states are accepted (if (count), if (name)), while objects and functions are rejected (always truthy). Values the static checker cannot prove are caught by runtime VM assertions. When types are statically proven, the compiler emits specialized opcodes (add_num, lt_num, etc.) that skip runtime type dispatch. See sound mode docs.

Compile-time evaluation. comptime() folds expressions at load time, modeled after Zig's comptime. Hash a version string, uppercase a constant, precompute a config value - all before the handler runs.

Automatic runtime sandboxing. Every precompiled handler is sandboxed by default. The compiler extracts a contract of what the handler does (env vars, outbound hosts, cache namespaces, SQL query names) and derives a least-privilege policy that restricts runtime access to exactly the proven values. No configuration required. -Dcontract additionally emits a contract.json manifest. -Dpolicy=policy.json overrides auto-derived sandboxing with an explicit policy. Non-literal arguments honestly report "dynamic": true. Self-extracting binaries parse the embedded contract at startup: proven env vars are validated (missing vars fail fast instead of causing a 500 on first request), proven routes reject non-matching requests at the HTTP layer before entering JS, and proven handler properties are logged for operator visibility.

Handler effect classification. Every virtual module function carries a compile-time effect annotation (read, write, or none). During contract extraction, the compiler folds those calls into a small internal effect summary and derives handler-level properties: pure, read-only, stateless, retry-safe, deterministic, idempotent (safe for at-least-once delivery), injection-safe (no unvalidated input in sinks), and state-isolated (no cross-request data leakage). The I/O depth bound (max virtual module calls per request) enables compile-time Lambda timeout derivation. These properties flow into deployment manifests, OWASP compliance mapping, and the build report with no extra annotations. Handlers proven deterministic and read_only also have their GET/HEAD responses cached at runtime and served from Zig memory without entering JS. The X-Zigttp-Proof-Cache: hit response header confirms a cache hit.

Full TypeScript type checking. The compiler checks type annotations, not just strips them. Variable types, function argument types, return types, property access on records, and virtual module function signatures are validated at build time. Generic type aliases (type Result<T> = { ok: boolean; value: T }) are instantiated when used in annotations. Object literals are structurally matched against declared interface and type alias return types. Optional types from virtual modules (env(), cacheGet(), parseBearer()) are narrowed in if-guards: if (x), if (!x) return, and if (x !== undefined) all narrow nullable bindings to their inner type. Discriminated unions narrow in if-conditions: if (r.kind === "err") { return; } narrows r to the remaining member afterward. Type guard functions (x is T) narrow in both if-branches and after assert statements. distinct type UserId = string creates nominal types that prevent cross-type assignment while unwrapping for operations. readonly fields reject assignment at compile time. Template literal types (`/api/${string}`) validate string patterns at build time. const bindings preserve literal types; : Type annotations validate assignability without widening. let bindings widen literals so reassignment works. Interface declarations with all-function members are nominal to prevent structural forgery.

Structured concurrent I/O. parallel() and race() from zigttp:io overlap outbound HTTP without async/await or Promises. Handler code stays synchronous and linear; concurrency happens in the I/O layer using OS threads. Three API calls at 50ms each complete in ~50ms total.

One-command deploy. zigttp deploy cross-compiles the handler to a Linux musl binary, packages it as an OCI image with proven-fact labels (proof level, env vars, egress hosts, cache namespaces, routes, handler properties), pushes it through the zigttp control plane, and provisions the service. No flags, no config files, no registry to set up. First run prompts for a Zigttp access token in the terminal; browser-based device login remains available as fallback. Drift detection blocks accidental replaces; --confirm acknowledges and proceeds. See docs/deploy-tutorial.md.

Deterministic replay. Record every I/O boundary during handler execution with --trace, then replay against a new handler version with --replay or -Dreplay at build time. Because virtual modules are the only I/O boundary, recording their inputs and outputs captures all external state. Handlers become deterministic pure functions of (Request, VirtualModuleResponses).

Durable execution. --durable <dir> enables crash recovery and long-running workflows via write-ahead oplog. Wrap work in run(key, fn) from zigttp:durable with an idempotency key; each I/O call is persisted before returning to the handler. On crash recovery, recorded results are replayed without touching the network. Completed runs are deduplicated by key. Durable runs can suspend with sleep(ms), sleepUntil(unixMs), or waitSignal(name) and resume when the timer fires or a signal arrives via signal(key, name, payload). A background scheduler polls for ready timers and signals using the same replay-safe recovery path.

Guard composition. guard() from zigttp:compose combined with the pipe operator (|>) composes handlers with pre/post guards at compile time. The parser desugars guard(auth) |> guard(log) |> handler |> guard(cors) into a single flat function with sequential if-checks - zero runtime overhead.

Behavioral contract. -Dcontract enumerates every execution path through the handler and embeds them in contract.json as structured behaviors. Each path records the route, branching conditions (which I/O calls succeed or fail), the I/O sequence, and the resulting HTTP status. The restricted JS subset has no back-edges and no exceptions, so path enumeration is finite and exhaustive. Comparing two behavioral contracts shows which paths were preserved, which changed response codes, which were removed, and which are new.

Proven evolution. -Dprove=contract.json:traces.jsonl compares two handler versions by diffing their contracts (surface and behavior) and replaying recorded traces. The upgrade verifier produces a four-value verdict: safe, safe_with_additions, breaking, or needs_review. It factors in behavioral path changes, property regressions with severity (critical/warning/info), and trace coverage gaps. Output: proof.json, proof-report.txt, and upgrade-manifest.json. The standalone zigts prove old.json new.json CLI compares contracts without rebuilding (exit 0 for safe, 1 for breaking, 2 for needs_review).

Proven live reload. --watch --prove watches handler files and hot-swaps them in-process on every save. The server recompiles the handler, extracts its behavioral contract, diffs it against the running version, and applies the change only when the upgrade verdict is safe or safe_with_additions. Breaking changes (removed routes, lost critical properties) block the swap and print the diff. --force-swap overrides the block. Without --prove, --watch hot-reloads without contract proof. Compilation errors keep the old handler running. Durable handlers refuse live swap because replay state depends on handler identity.

Contract-driven mock server. zigts mock tests.jsonl --port 3001 serves mock HTTP responses from PathGenerator test cases. Frontend teams get a mock API provably consistent with the handler contract.

Generated tests. zigts gen-tests handler.ts -o handler.test.jsonl writes a JSONL test file from the same behavioral path enumeration the compiler uses internally. Each proven execution path becomes one runnable test case: synthesized request, ordered I/O stubs, expected status. The output runs immediately via --test or zigts mock. The zigts expert agent calls this automatically after edits so the test suite stays in sync with the proof.

Native modules over JS polyfills. Common FaaS needs (JWT auth, JSON Schema validation, caching, crypto) are implemented in Zig and exposed as zigttp:* virtual modules with zero interpretation overhead. Each module binding declares what runtime capabilities its implementation uses (clock, crypto, stderr, etc.); checked helpers enforce the declarations at call time, so implementation drift panics instead of silently misbehaving.

Numbers

3ms runtime init. 1.2MB binary. 4MB memory baseline. Pre-warmed handler pool with per-request isolation. See performance docs for cold start breakdowns and deployment patterns.

Install

Pre-built binaries for macOS and Linux (x86_64, aarch64):

curl -fsSL https://raw.githubusercontent.com/srdjan/zigttp/main/install.sh | sh

Or download a tarball from GitHub Releases.

To build from source (requires Zig 0.16.0):

git clone https://github.com/srdjan/zigttp.git && cd zigttp
zig build -Doptimize=ReleaseFast

Quick Start

# Run with inline handler
./zig-out/bin/zigttp serve -e "function handler(r) { return Response.json({hello:'world'}) }"

# Or with a handler file
./zig-out/bin/zigttp serve examples/handler/handler.ts

Test it:

curl http://localhost:8080/

Handler Example

function HomePage() {
    return (
        <html>
            <head>
                <title>Hello World</title>
            </head>
            <body>
                <h1>Hello World</h1>
                <p>Welcome to zigttp!</p>
            </body>
        </html>
    );
}

function handler(request) {
    if (request.url === "/") {
        return Response.html(renderToString(<HomePage />));
    }

    if (request.url === "/api/echo") {
        return Response.json({
            method: request.method,
            url: request.url,
            body: request.body,
        });
    }

    return Response.text("Not Found", { status: 404 });
}

HTMX Example

zigttp includes native support for HTMX attributes in JSX:

function TodoForm() {
    return (
        <form
            hx-post="/todos"
            hx-target="#todo-list"
            hx-swap="beforeend">
            <input type="text" name="text" required />
            <button type="submit">Add Todo</button>
        </form>
    );
}

function handler(request) {
    if (request.url === "/" && request.method === "GET") {
        return Response.html(renderToString(<TodoForm />));
    }

    if (request.url === "/todos" && request.method === "POST") {
        // Parse form data, create todo item
        const todoHtml = renderToString(
            <div class="todo-item">New todo item</div>
        );
        return Response.html(todoHtml);
    }

    return Response.text("Not Found", { status: 404 });
}

See examples/htmx-todo/ for a complete HTMX application.

Virtual Modules

zigttp provides native virtual modules via import { ... } from "zigttp:*" syntax. These run as native Zig code with zero JS interpretation overhead.

Most modules live in their own peer package packages/modules/, depending only on packages/zigttp-sdk/. The zigts engine imports the module bindings at comptime and adapts them through module_binding_adapter.zig. A handful of modules (io, scope, durable, plus the install-shim portions of sql, fetch, service, websocket) still live in packages/zigts/src/modules/ because they require direct runtime integration (GC roots, threadlocal fetch state, runtime bootstrap).

Available Modules

Modules are grouped by role; each cell in the Module column links to the per-module page. See docs/virtual-modules/README.md for the landing page and category index.

Module Exports Description
zigttp:env env Environment variable access
zigttp:crypto sha256, hmacSha256, base64Encode, base64Decode Cryptographic functions
zigttp:router routerMatch Pattern-matching HTTP router
zigttp:auth parseBearer, jwtVerify, jwtSign, verifyWebhookSignature, timingSafeEqual JWT auth and webhook verification
zigttp:validate schemaCompile, validateJson, validateObject, coerceJson, schemaDrop JSON Schema validation with format validators (email, uuid, iso-date, iso-datetime)
zigttp:decode decodeJson, decodeForm, decodeQuery Schema-backed typed ingress for JSON, URL-encoded forms, and query strings
zigttp:cache cacheGet, cacheSet, cacheDelete, cacheIncr, cacheStats In-memory key-value cache with TTL and LRU
zigttp:sql sql, sqlOne, sqlMany, sqlExec Registered SQLite queries with build-time schema validation
zigttp:service serviceCall Named internal service-to-service calls backed by system.json
zigttp:fetch fetch Web-standard outbound HTTP with optional durable replay
zigttp:websocket send, close, serializeAttachment, deserializeAttachment, getWebSockets, roomFromPath, setAutoResponse WebSocket protocol termination with rooms, attachments, and codec-level auto-replies
zigttp:io parallel, race Structured concurrent I/O (overlaps fetchSync calls using OS threads)
zigttp:scope scope, using, ensure Structured lifecycle management with deterministic cleanup at scope exit
zigttp:compose guard, pipe Compile-time handler composition via pipe operator
zigttp:durable run, step, stepWithTimeout, sleep, sleepUntil, waitSignal, signal, signalAt Durable execution with crash recovery, timers, signals, and timeout-aware steps
zigttp:url urlParse, urlSearchParams, urlEncode, urlDecode URL parsing and query string encoding
zigttp:id uuid, ulid, nanoid ID generation (UUID v4, ULID, NanoID)
zigttp:http parseCookies, setCookie, negotiate, parseContentType, cors HTTP utilities for cookies, content negotiation, CORS
zigttp:log logDebug, logInfo, logWarn, logError Structured logging to stderr
zigttp:text escapeHtml, unescapeHtml, slugify, truncate, mask String utilities for HTML escaping, slugs, redaction
zigttp:time formatIso, formatHttp, parseIso, addSeconds Time formatting and arithmetic
zigttp:ratelimit rateCheck, rateReset Per-key rate limiting with sliding windows

Each export carries an effect annotation used for handler property classification. See docs/virtual-modules/README.md for the full read/write/none breakdown across all 22 modules.

Each module binding also declares the runtime capabilities its Zig implementation consumes (clock, random, crypto, stderr, filesystem, sqlite, env, runtime_callback, policy_check, network). These declarations are governance metadata for the module internals and do not affect the handler-facing effect classification or sandbox policy. Capability-checked helpers enforce them at call time: if a module reads the system clock but its binding omits clock, the call panics with a diagnostic naming the module and the missing capability.

Module Runtime Capabilities
zigttp:auth crypto, clock
zigttp:cache clock, policy_check
zigttp:crypto crypto
zigttp:env env, policy_check
zigttp:id clock, random
zigttp:io runtime_callback
zigttp:log clock, stderr
zigttp:ratelimit clock
zigttp:scope runtime_callback
zigttp:service filesystem, runtime_callback
zigttp:sql sqlite, policy_check
zigttp:durable runtime_callback
zigttp:compose, zigttp:decode, zigttp:http, zigttp:router, zigttp:text, zigttp:time, zigttp:url, zigttp:validate (none)

Extension modules (via zigttp-sdk) declare capabilities the same way; the same checked helpers enforce them. Modules declaring no capabilities skip the enforcement wrapper at compile time.

Auth Example

import { parseBearer, jwtVerify, jwtSign } from "zigttp:auth";

function handler(req: Request): Response {
    const token = parseBearer(req.headers["authorization"]);
    if (token === undefined) return Response.json({ error: "unauthorized" }, { status: 401 });

    const result = jwtVerify(token, "my-secret");
    if (!result.ok) return Response.json({ error: result.error }, { status: 401 });

    return Response.json({ user: result.value });
}

Validation Example

import { schemaCompile } from "zigttp:validate";
import { decodeJson } from "zigttp:decode";

schemaCompile("user", JSON.stringify({
    type: "object",
    required: ["name", "email"],
    properties: {
        name: { type: "string", minLength: 1, maxLength: 100 },
        email: { type: "string", minLength: 5 },
        age: { type: "integer", minimum: 0, maximum: 200 }
    }
}));

function handler(req: Request): Response {
    if (req.method === "POST") {
        const result = decodeJson("user", req.body ?? "{}");
        if (!result.ok) return Response.json({ errors: result.errors }, { status: 400 });
        return Response.json({ user: result.value }, { status: 201 });
    }
    return Response.json({ ok: true });
}

Use decodeJson, decodeForm, and decodeQuery as the default request-ingress helpers when the payload shape is schema-backed. validateJson and coerceJson remain available for lower-level validation flows.

Cache Example

import { cacheGet, cacheSet, cacheStats } from "zigttp:cache";

function handler(req: Request): Response {
    const cached = cacheGet("api", req.url);
    if (cached !== undefined) return Response.json(JSON.parse(cached));

    const data = { message: "computed", path: req.url };
    cacheSet("api", req.url, JSON.stringify(data), 60); // TTL: 60 seconds

    return Response.json(data);
}

Concurrent I/O Example

import { parallel, race } from "zigttp:io";

function handler(req: Request): Response {
    // Three API calls in ~50ms instead of ~150ms
    const [user, orders, inventory] = parallel([
        () => fetchSync("https://users.internal/api/v1/123"),
        () => fetchSync("https://orders.internal/api/v1?user=123"),
        () => fetchSync("https://inventory.internal/api/v1/789")
    ]);

    return Response.json({
        user: user.json(),
        orders: orders.json(),
        inventory: inventory.json()
    });
}

See examples/modules/modules_all.ts for an integration example using all modules together.

SQL Example

import { sql, sqlMany, sqlExec } from "zigttp:sql";

sql("listTodos", "SELECT id, title, done FROM todos ORDER BY id ASC");
sql("createTodo", "INSERT INTO todos (title, done) VALUES (:title, 0)");

function handler(req: Request): Response {
    if (req.method === "GET") {
        return Response.json({ items: sqlMany("listTodos") });
    }

    const body = JSON.parse(req.body);
    return Response.json(sqlExec("createTodo", { title: body.title }), { status: 201 });
}

Build-time validation requires a schema snapshot:

zig build -Dhandler=examples/sql/sql-crud.ts -Dsql-schema=examples/sql/schema.sql

CLI Options

zigttp serve [options] <handler.js>

Options:
  -p, --port <PORT>     Port (default: 8080)
  -h, --host <HOST>     Host (default: 127.0.0.1)
  -e, --eval <CODE>     Inline JavaScript handler
  -m, --memory <SIZE>   JS runtime memory limit (default: 0 = no limit)
  -n, --pool <N>        Runtime pool size (default: auto)
  --cors                Enable CORS headers
  --static <DIR>        Serve static files
  --outbound-http       Enable native outbound bridge (fetchSync/httpRequest)
  --outbound-host <H>   Restrict outbound bridge to exact host H
  --outbound-timeout-ms Connect timeout for outbound bridge (ms)
  --outbound-max-response <SIZE>
  --watch               Watch handler files and hot-swap on change
  --prove               With --watch: diff behavioral contracts before swapping
  --force-swap          With --watch --prove: apply breaking changes anyway
  --trace <FILE>        Record handler I/O traces to JSONL file
  --replay <FILE>       Replay recorded traces and verify handler output
  --sqlite <FILE>       SQLite database path for zigttp:sql
  --durable <DIR>       Enable durable execution with write-ahead oplog
  --system <FILE>       System registry for zigttp:service
  --no-env-check        Skip startup env var validation (development use)

zigttp deploy

Cross-compiles the handler to a Linux musl binary, packages it as an OCI image, pushes it through the zigttp control plane, and provisions the service. The control plane mints short-lived registry credentials per deploy and forwards the image to the upstream provider, so there is no account to create, no registry to configure, and no API token to manage on the client.

zigttp deploy [options]

Options:
  --region <name>   Override the deployment region for this run
  --confirm         Acknowledge drift and proceed with a replace-like update
  --wait            Block until the service reports ready (default)
  --no-wait         Return immediately after the deploy is accepted
  -h, --help        Show usage

If credentials are missing, the CLI first prompts for a Zigttp access token directly in the terminal. The intended hosted flow is to create that token in zigttp-admin, then paste it into the CLI. Submit an empty token to fall back to browser-based device login. Tokens are stored at ~/.zigttp/credentials; zigttp logout clears them. You can also sign in ahead of time with zigttp login or zigttp login --token-stdin. The control plane base URL defaults to https://api.zigttp.dev; set ZIGTTP_CONTROL_PLANE_URL to point at a self-hosted instance.

Everything else is auto-detected from the current directory:

  • Handler file: first match of handler.ts, handler.tsx, handler.jsx, handler.js, or the same paths under src/.
  • Service name: the name field in package.json, then the basename of the git origin remote, then the current directory name. Slugified to lowercase with dashes.
  • Runtime environment: KEY=value pairs from .env in the current directory, one per line. Missing file is fine; a malformed line aborts the deploy with a path:line diagnostic.
  • Region: --region if given, then the region from the previous deploy of this service, then us-central.

Before the CLI requests deploy credentials, it compiles the handler contract and sends that contract plus its canonical SHA-256 to the control plane. A self-hosted control plane can use that to auto-approve safe changes, auto-approve previously granted risky changes, or return a review URL when a deploy needs capability approval before continuing.

zigttp deploy
zigttp deploy --region eu-west
zigttp deploy --no-wait
zigttp review <plan-id>
zigttp review <plan-id> --approve --grant
zigttp grants
zigttp revoke-grant <grant-id>

Reconciliation reads .zigttp/deploy-state.json, which stores non-secret identifiers (scope, region, plan, managed env keys, last image digest) from the last successful deploy of each service. A change to scope, region, plan, or removal of a previously managed env var is flagged as drift; the CLI prints the warning and exits with code 2. Re-run with --confirm to acknowledge and proceed. Even with --confirm, the old service is rebound and updated, never deleted.

After the push the CLI polls the provider until the service reports ready (120s default). --no-wait skips the poll. Exit codes:

  • 0 success
  • 2 drift detected, re-run with --confirm
  • 3 timed out waiting for the service to report ready
  • 4 service failed to start

OCI image references are content-addressed by the manifest digest, which is printed alongside the public URL on success. Identical handlers produce identical digests. Proof facts from the handler contract (proof level, env var names, egress hosts, cache namespaces, routes, handler properties) are encoded as JSON arrays in OCI image labels so provenance survives in the registry.

zigts CLI (compiler and analyzer)

Standalone analysis and compilation without starting a server.

zigts expert calls the Anthropic API directly (ANTHROPIC_API_KEY). The persona, reference material, skill catalog, prompt templates, themes, and compiler metadata are all baked into the binary; there is no runtime plugin surface. The one external input that reaches the system prompt is AGENTS.md / CLAUDE.md, walked up from cwd to the enclosing .git/ directory and appended as a labelled read-only project-context section with a 128 KiB cap. Disable with --no-context-files. Full pi architecture: packages/pi/README.md.

# Interactive REPL
zigts expert
zigts expert --resume                    # resume newest session for this cwd
zigts expert --continue                  # alias for --resume
zigts expert --session-id <id>           # named or resumed session
zigts expert --fork <session-id>         # branch from an existing session
zigts expert --yes                       # auto-approve all verified edits
zigts expert --no-edit                   # auto-reject all verified edits
zigts expert --no-context-files          # skip AGENTS.md / CLAUDE.md load
zigts expert --tools minimal             # workspace-read-only tool preset
zigts expert --tools full                # full compiler tool preset (default)

# Non-interactive (one turn and exit)
zigts expert --print "add a GET /health route"
zigts expert --print "..." --mode json   # NDJSON event stream to stdout

# Line-delimited JSON-RPC 2.0 over stdio (long-lived session)
zigts expert --mode rpc

In --mode json, each event is {"v":1,"k":"<kind>","d":<payload>}. Kinds: user_text, model_text, tool_use, tool_result, proof_card, diagnostic_box, system_note, end. Persisted events.jsonl lines use the same envelope for transcript events, but end is a live-stream-only sentinel emitted last on success. Errored runs may terminate without end, and session files do not persist it.

In --mode rpc, each stdin line is a JSON-RPC 2.0 request; each stdout response line is a result or error keyed by id. Methods: turn, compact, session.info, tools.list, tools.invoke, skills.list, templates.{list,expand}, model.{list,set}, shutdown. During a turn, each new transcript entry emits as a notification with method "event" before the final result lands.

The interactive REPL accepts slash commands alongside natural language:

/compact                       collapse the session transcript into a summary
/fork                          branch the current session into a new directory
/tree                          list all sessions for this workspace
/resume  /continue             reload the newest session for this cwd
/new                           start a fresh session
/model                         show the active model and available IDs
/model <id>                    switch to a different model mid-session
/skills                        list available skill shortcuts
/skill:<name>                  send the named skill body as a prompt
/templates                     list available prompt templates
/template:<name> [args...]     expand a template and send it as a prompt
/settings                      show compile-time defaults (model, token limits)
/settings theme                list available TUI themes
/settings theme <name>         switch the session's TUI theme
/hotkeys                       list keyboard shortcuts
/changelog                     recent expert subsystem additions

After each model turn the REPL prints cumulative token use for the session: [tokens: in=N cache_r=N cache_w=N out=N]. The totals reset when you start a new session or reload with /new or /resume.

The interactive surface runs as a bottom-anchored TUI: the status line (session, model, token totals) and input line stay pinned at the bottom while scrollback flows above. Redraws wrap in CSI ?2026h / ?2026l synchronized-output sequences so supporting terminals render each frame atomically. Two themes ship (default, solarized-dark); swap mid-session with /settings theme <name>.

On resume, the session's meta.json carries the policy_hash it was created under. If the current binary's hash differs, the REPL prepends a [policy drift] system note to the transcript so the model knows prior rule citations may be stale against today's compiler.

zigts check [handler.ts] [options]    # Verify handler, show proof card
zigts compile [--system path] <handler.ts> <out.zig>  # Compile to embedded bytecode
zigts prove <old.json> <new.json>     # Compare contracts (exit 0=safe, 1=breaking)
zigts mock <tests.jsonl> [--port N]   # Mock server from test cases
zigts link <system.json>              # Cross-handler contract linking
zigts features [--json]               # List allowed/blocked language features
zigts modules [--json]                # List virtual modules and exports
zigts meta [--json]                   # Policy metadata (version, hash, rule count)
zigts gen-tests [handler.ts] [-o output.jsonl]  # Generate tests from proven paths
zigts verify-paths <f>... [--json]    # Full analysis on files
zigts verify-modules --builtins --strict --json  # Governance audit

Structured JSON output

zigts check --json handler.ts writes machine-readable diagnostics to stdout. Add --system system.json when the handler uses serviceCall() and you want compile-time typing for internal service responses and request-shape validation.

{
  "success": false,
  "diagnostics": [{
    "code": "ZTS001",
    "severity": "error",
    "message": "'try/catch' is not supported",
    "file": "handler.ts",
    "line": 23,
    "column": 3,
    "suggestion": "use Result types for error handling"
  }]
}

On success, the output includes a proof summary: env vars, outbound hosts, virtual modules, and handler properties.

Code ranges: ZTS0xx (parser), ZTS1xx (sound mode), ZTS2xx (type checker), ZTS3xx (handler verifier).

Key Features

Performance: Type-prefix NaN-boxing (single-instruction type checks), index-based hidden classes with SoA layout and O(1) transition lookups, polymorphic inline cache (PIC), generational GC, hybrid arena allocation for request-scoped workloads.

HTTP/FaaS Optimizations: Shape preallocation for Request/Response objects, pre-interned HTTP atoms, HTTP string caching, LockFreePool handler isolation, zero-copy response mode.

Compile-Time Analysis: Handler verification (-Dverify) proves correctness at build time. Contract extraction with behavioral paths and auto-sandboxing restrict runtime capabilities to proven values. Proven properties also control runtime behavior: deterministic+read_only handlers have their responses cached and served without JS execution. zigttp:sql queries are prepared against a build-time schema snapshot via -Dsql-schema=.... Sound mode rejects non-numeric arithmetic, mixed-type +, and tautological comparisons at compile time, and emits type-specialized opcodes when types are proven. TypeScript type checking validates annotations, narrows optionals through if-guards, and structurally matches object literals against declared types.

Structured Concurrency: parallel() and race() overlap outbound HTTP using OS threads. No async/await, no event loop - handler code stays synchronous and linear.

Deployment Pipeline: Contract manifests with behavioral paths (-Dcontract), one-command runtime deploy (zigttp deploy), auto-derived runtime sandboxing, deterministic replay (--trace/--replay/-Dreplay), proven evolution with upgrade verdicts (-Dprove), proven live reload (--watch --prove), and durable execution (--durable) form a pipeline from source analysis to production deployment with crash recovery.

Language Support: ES5 + select ES6 features (for...of with break/continue, typed arrays, exponentiation, pipe operator, compound assignments), native TypeScript/TSX stripping with type checking, compile-time evaluation with comptime(), direct JSX parsing, match expression, assert statement, distinct type, readonly fields, template literal types, type guards (x is T).

JIT Compilation: Baseline JIT for x86-64 and ARM64, inline cache integration, object literal shapes, type feedback, adaptive compilation.

Virtual Modules: Native zigttp:auth (JWT/HS256, webhook signatures), zigttp:validate (JSON Schema registry), zigttp:decode (typed request ingress), zigttp:cache (TTL/LRU key-value store), zigttp:service (named internal service calls), zigttp:io (structured concurrent I/O), zigttp:scope (deterministic cleanup tied to lexical scope), zigttp:compose (guard composition), zigttp:durable (crash recovery, timers, signals), plus zigttp:env, zigttp:crypto, zigttp:router. Module bindings declare runtime capabilities (clock, crypto, stderr, etc.) enforced by shared checked helpers, with a comptime short-circuit that skips the wrapper for modules declaring no capabilities.

Developer Experience: Fetch-like HTTP surface (Response.*, Response(body, init?), Request(url, init?), Headers(init?), request.text(), request.json(), headers.get(), fetchSync()), console methods (log, error, warn, info, debug), static file serving with LRU cache, CORS support, pool metrics.

Native Outbound Bridge

When enabled with --outbound-http, handlers can call the higher-level fetchSync() helper:

const resp = fetchSync("http://127.0.0.1:8787/v1/ops?view=state", {
  method: "GET",
  headers: { Authorization: "Bearer ..." }
});

const data = resp.json();

fetchSync() returns a response-shaped object with status, ok, headers.get(name), text(), and json().

Current helper semantics:

  • headers.get(name) is case-insensitive and returns the last observed value for that header name, or null.
  • Headers(init?), Request(url, init?), and Response(body, init?) are available as factory-style HTTP types. new is not supported by the parser, so call them as plain functions.
  • Headers instances support get(name), set(name, value), append(name, value), has(name), and delete(name).
  • text() returns the raw body string, or "" when no body is present.
  • json() returns parsed JSON, or undefined when the body is empty or invalid JSON.
  • Body readers are single-use. Once text() or json() is called on a request/response object, subsequent body reads throw. Use request.body if you need the raw body string without consuming it.
  • Validation, allowlist, network, timeout, and size-limit failures do not throw into handler code; fetchSync() returns a 599 response with a JSON body containing error and details.

The lower-level httpRequest(jsonString) bridge remains available:

const raw = httpRequest(JSON.stringify({
  url: "http://127.0.0.1:8787/v1/ops?view=state",
  method: "GET",
  headers: { Authorization: "Bearer ..." }
}));

httpRequest returns JSON with either { ok: true, status, reason, body, content_type? } or { ok: false, error, details }. Use --outbound-host to restrict egress to a single host.

Internal Service Calls

For internal zigttp-to-zigttp calls, prefer serviceCall() from zigttp:service over hard-coded internal URLs:

import { serviceCall } from "zigttp:service";

function handler(req: Request): Response {
  const user = serviceCall("users", "GET /api/users/:id", {
    params: { id: "123" },
  });

  if (user.status !== 200) {
    return Response.json({ error: "user service unavailable" }, { status: 502 });
  }

  return Response.json(user.json());
}

serviceCall(serviceName, "METHOD /path", init?) resolves through system.json, lowers to the existing outbound bridge, and gives zigts link a first-class internal edge to verify.

With zigts check --system <FILE> or zigts compile --system <FILE>, literal serviceCall() sites also become payload-aware at compile time:

  • status narrows to the target route's proven status codes
  • .json() returns the target route's proven JSON type when there is a single compatible response schema
  • if different status codes produce different schemas, narrow on resp.status before calling .json()
  • required path/query/header/body inputs are validated against the target route contract during type checking

init supports:

  • params for :path placeholders
  • query for query string keys
  • headers for request headers
  • body for string request bodies

Run handlers that use zigttp:service with --system <FILE>:

zigttp serve --system examples/system/system.json examples/system/gateway.ts

system.json now requires a stable name for each handler:

{
  "version": 1,
  "handlers": [
    { "name": "gateway", "path": "examples/system/gateway.ts", "baseUrl": "https://gateway.internal" },
    { "name": "users", "path": "examples/system/users.ts", "baseUrl": "https://users.internal" }
  ]
}

zigts link <system.json> uses those names and routes to prove internal service composition. Raw fetchSync("https://users.internal/...") still works, but named service calls produce stronger linking and clearer diagnostics.

The linker now reports payload proof separately from route proof. proofLevel keeps its current meaning, while system-contract.json and system-report.txt add explicit payload fields:

  • payloadProven
  • payloadCompatible
  • payloadDetail

zigts rollout <old-system.json> <new-system.json> extends the same proof pipeline across time. It compiles and links both systems, checks mixed-version states, and emits rollout-plan.json plus rollout-report.txt with the smallest rollout phases it can prove. If a change only becomes safe when multiple handlers move together, the planner collapses them into one coordinated phase instead of pretending they can deploy independently.

Compile-Time Toolchain

zigttp's compile-time toolchain goes beyond precompilation. It verifies correctness, checks types, extracts a capability contract, auto-derives a runtime sandbox, and can generate platform-specific deployment configs - all at build time.

# Development build (runtime handler loading)
zig build -Doptimize=ReleaseFast

# Production build (embedded bytecode, auto-sandboxed, 16% faster cold starts)
zig build -Doptimize=ReleaseFast -Dhandler=examples/handler/handler.ts

# Verify handler correctness at compile time
zig build -Dhandler=handler.ts -Dverify

# Emit contract manifest (what the handler is allowed to do)
zig build -Dhandler=handler.ts -Dcontract

# Validate zigttp:sql queries against a schema snapshot
zig build -Dhandler=examples/sql/sql-crud.ts -Dsql-schema=examples/sql/schema.sql

# Override auto-derived sandbox with an explicit capability policy
zig build -Dhandler=handler.ts -Dpolicy=policy.json

# Replay-verify handler against recorded traces before embedding
zig build -Dhandler=handler.ts -Dreplay=traces.jsonl

# Compare handler versions (equivalent, additive, or breaking)
zig build -Dhandler=handler.ts -Dprove=old-contract.json:traces.jsonl

# Plan a safe multi-handler rollout between two system definitions
./zig-out/bin/zigts rollout old/system.json new/system.json

# Combine build-time verification passes
zig build -Doptimize=ReleaseFast -Dhandler=handler.ts -Dverify -Dcontract

# External enrichment flags (optional, for code generator integration)
zig build -Dhandler=handler.ts -Dmanifest=governance-manifest.json
zig build -Dhandler=handler.ts -Dexpect-properties=properties.json
zig build -Dhandler=handler.ts -Ddata-labels=data-labels.json
zig build -Dhandler=handler.ts -Dfault-severity=fault-severity.json
zig build -Dhandler=handler.ts -Dreport=json

Handler Verification (-Dverify)

The verifier statically proves seven properties of your handler at compile time:

  1. Every code path returns a Response. Missing else branches and paths that fall through without returning are caught.
  2. Result values are checked before access. Calls like jwtVerify, decodeJson, and decodeQuery return Result objects. The verifier ensures .ok is checked before .value is accessed.
  3. No unreachable code. Statements after an unconditional return produce a warning.
  4. No unused variables. Declared variables that are never referenced produce a warning. Suppress with an underscore prefix (_unused).
  5. Match expressions have default arms. A match without a default arm produces a warning.
  6. Optional values are checked before use. Values from env(), cacheGet(), parseBearer(), and routerMatch() must be narrowed via if (val), val !== undefined, val ?? default, or reassignment before use in expressions.
  7. No cross-request state leakage. Module-scope variables must not be mutated inside the handler body. This prevents one request from affecting another through shared mutable state.

zigttp's JS subset eliminates back-edges: no while, no switch, no try/catch, no exceptions. break and continue are allowed within for-of (forward jumps only). The IR tree is the control flow graph. Verification is a recursive tree walk, not a fixpoint dataflow analysis.

$ zig build -Dhandler=handler.ts -Dverify

verify error: not all code paths return a Response
  --> handler.ts:2:17
   |
  2 | function handler(req) {
   |                 ^
   = help: ensure every branch (if/else) ends with a return statement

See docs/verification.md for the full specification.

Contract Manifest and Auto-Sandboxing

Every precompilation automatically extracts a contract from the handler's IR. The contract describes what your handler does before it runs and is used to derive the runtime sandbox. Add -Dcontract to also emit it as contract.json. It extracts:

  • Virtual modules imported and which functions are used
  • Environment variables accessed via env("NAME") - literal names are enumerated, dynamic access is flagged
  • Outbound hosts called via fetchSync("https://...") - hosts are extracted from URL literals
  • Internal service calls made via serviceCall("name", "METHOD /path", init) - service names, route signatures, and statically proven params/query/header/body keys are captured
  • System-linked payload facts for named internal edges - target response statuses, JSON payload proof, and payload-proof gaps are reported in system-level output
  • Cache namespaces used by cacheGet/cacheSet/etc.
  • SQL queries registered with sql("name", "...") - names, statement kinds, and touched tables are captured after schema validation
  • Scope usage from scope("name", fn) - literal scope names, whether scope callbacks stay dynamic, and the maximum nested scope depth are captured
  • API surface from proven routes: method/path, path/query/header params, JSON request bodies, response variants, and bearer auth metadata
  • Handler properties derived from the internal effect summary of virtual module calls, cache reads, scope usage, egress, and nondeterministic builtins (pure, read_only, stateless, retry_safe, deterministic)
  • Behavioral paths - every execution path through the handler with route, branching conditions, I/O sequence, and response status (exhaustive when the path count stays below 1024)
  • Verification results (when combined with -Dverify)
  • Route patterns (when combined with -Daot)
{
  "version": 12,
  "modules": ["zigttp:auth", "zigttp:cache", "zigttp:scope"],
  "functions": {
    "zigttp:auth": ["jwtVerify", "parseBearer"],
    "zigttp:cache": ["cacheGet", "cacheSet"],
    "zigttp:scope": ["scope", "ensure"]
  },
  "env": { "literal": ["JWT_SECRET"], "dynamic": false },
  "egress": { "hosts": ["api.example.com"], "dynamic": false },
  "cache": { "namespaces": ["sessions"], "dynamic": false },
  "scope": {
    "used": true,
    "names": ["request", "enrich-user"],
    "dynamic": false,
    "maxDepth": 2
  },
  "sql": {
    "backend": "sqlite",
    "queries": [
      { "name": "listTodos", "operation": "select", "tables": ["todos"] }
    ],
    "dynamic": false
  },
  "properties": {
    "pure": false,
    "readOnly": false,
    "stateless": false,
    "retrySafe": false,
    "deterministic": true,
    "hasEgress": true
  },
  "behaviors": [
    {
      "method": "GET",
      "pattern": "/users/:id",
      "status": 200,
      "ioDepth": 2,
      "failurePath": false,
      "conditions": [
        {"kind": "io_ok", "module": "auth", "func": "jwtVerify"},
        {"kind": "io_ok", "module": "cache", "func": "cacheGet"}
      ],
      "ioSequence": [
        {"module": "auth", "func": "jwtVerify"},
        {"module": "cache", "func": "cacheGet"}
      ]
    },
    {
      "method": "GET",
      "pattern": "/users/:id",
      "status": 401,
      "ioDepth": 1,
      "failurePath": true,
      "conditions": [
        {"kind": "io_fail", "module": "auth", "func": "jwtVerify"}
      ],
      "ioSequence": [
        {"module": "auth", "func": "jwtVerify"}
      ]
    }
  ],
  "behaviorsExhaustive": true
}

The "dynamic": false fields are the key signal. They mean "we can enumerate every value statically." When a handler uses a variable instead of a string literal (env(someVar) instead of env("JWT_SECRET")), the contract honestly reports "dynamic": true.

OpenAPI and TypeScript SDK

The same proven route facts can also be emitted as OpenAPI and as a generated TypeScript client:

zig build -Dhandler=examples/routing/api-surface.ts -Dcontract -Dopenapi -Dsdk=ts

This writes three sibling artifacts in src/generated/:

  • contract.json
  • openapi.json
  • client.ts

The current API emitters include facts the compiler can prove without guessing:

  • route method and path
  • path, query, and header params reached through literal access
  • proven JSON request bodies from validateJson(...), coerceJson(...), and decodeJson(...)
  • proven form request bodies from decodeForm(...)
  • typed query params from decodeQuery(...)
  • proven response variants, including multiple status codes when statically visible
  • bearer auth metadata
  • x-zigttp-* hints whenever part of the surface stays dynamic

The generated SDK only exposes typed helpers for routes it can prove end to end. Everything else remains available through requestRaw() and is listed in skippedOperations. A fully proven route can be consumed like this:

import { createClient } from "./src/generated/client";

const api = createClient({ baseUrl: "https://api.example.com" });

const result = await api.postProfilesId({
    params: { id: "user_123" },
    query: { verbose: true },
    body: { displayName: "Ada" },
    headers: { "x-client-id": "cli-42" },
});

console.log(result.data.displayName);

Auto-sandboxing: The contract is used to derive a RuntimePolicy embedded in the binary. Sections with dynamic: false are restricted to exactly the proven literals. Sections with dynamic: true remain unrestricted. The build reports what was proven:

Sandbox: complete (all access statically proven)
  env: restricted to [JWT_SECRET] (1 proven, no dynamic access)
  egress: restricted to [api.example.com] (1 proven, no dynamic access)
  cache: restricted to [sessions] (1 proven, no dynamic access)
  sql: restricted to [listTodos] (1 proven, no dynamic access)
Handler Properties:
  ---    pure            handler is a deterministic function of the request
  ---    read_only       no state mutations via virtual modules
  ---    stateless       independent of mutable state
  ---    retry_safe      disabled when scope-managed cleanup or bare writes are present
  PROVEN deterministic   no Date.now() or Math.random()

Explicit Policy Override (-Dpolicy)

To override auto-derived sandboxing with a stricter or different policy, pass an explicit policy file. The policy is validated against the contract at build time and enforced at runtime. Local file-import handlers are covered: capability usage is aggregated across the module graph before validation.

{
  "env": { "allow": ["JWT_SECRET"] },
  "egress": { "allow_hosts": ["api.example.com"] },
  "cache": { "allow_namespaces": ["sessions"] },
  "sql": { "allow_queries": ["listTodos"] }
}

Omit a section to leave that capability unrestricted. If a section is present, dynamic access in that category is rejected because zigttp cannot fully enumerate it.

Native Deploy

zigttp deploy consumes the same compiler-proven contract used for sandboxing and verification, then ships the handler through the zigttp control plane in one command. The full flow lives in docs/deploy-tutorial.md; the short version is above under zigttp deploy.

Proof facts from the contract are encoded as JSON arrays in OCI image labels (zigttp.proof-level, zigttp.env-vars, zigttp.egress-hosts, zigttp.cache-namespaces, zigttp.routes, plus boolean labels for retry-safe, read-only, idempotent, and so on) so registries and downstream tooling can audit what the binary is allowed to do without running it. Proof levels: complete (all checks pass, no dynamic flags), partial (some verification but dynamic access detected), none (no verification ran).

Deterministic Replay (--trace / --replay / -Dreplay)

zigttp's restricted JS subset (no async, no exceptions, no side-effecting builtins) makes handlers deterministic pure functions of their request and virtual module responses. The replay system exploits this property.

Record traces during normal operation:

zigttp serve handler.ts --trace traces.jsonl

Every virtual module call, fetchSync response, Date.now() timestamp, and Math.random() value is recorded alongside the request and response.

Replay traces against a modified handler to detect regressions:

zigttp serve --replay traces.jsonl handler-v2.ts

Reports identical, status-changed, and body-changed results with structured diffs.

Build-time replay fails the build if regressions are detected:

zig build -Dhandler=handler-v2.ts -Dreplay=traces.jsonl

Durable Execution (--durable)

Enable crash recovery with a write-ahead oplog:

zigttp serve handler.ts --durable ./oplogs

Handlers opt into durability via the zigttp:durable virtual module:

import { run, step, stepWithTimeout, sleep, waitSignal, signal } from "zigttp:durable";

function handler(req: Request): Response {
    // Deliver a signal to a waiting run
    if (req.url === "/approve") {
        signal("order:42", "approved", { by: "admin" });
        return Response.json({ ok: true });
    }

    // Durable workflow with steps, timers, and signals
    return run("order:42", () => {
        const order = step("create", () =>
            fetchSync("https://api.internal/orders", { method: "POST", body: "{}" }));
        sleep(5000);
        const approval = waitSignal("approved");
        const confirmed = step("confirm", () =>
            fetchSync("https://api.internal/orders/42/confirm", {
                method: "POST", body: JSON.stringify(approval)
            }));
        return Response.json(confirmed.json());
    });
}

run(key, fn) wraps a unit of work with an idempotency key. Each step(name, fn) persists its result to the oplog before returning to the handler. sleep(ms) and sleepUntil(unixMs) suspend the run until a timer fires. waitSignal(name) suspends until a signal arrives via signal(key, name, payload) or signalAt(key, name, unixMs, payload). Pending runs return 202 Accepted with a JSON body describing the wait. On crash recovery, recorded results are replayed without touching the network. A background scheduler polls for ready timers and signals. Completed runs are deduplicated by key. stepWithTimeout(name, timeoutMs, fn) executes a step with a deadline - returns { ok: true, value } on completion or { ok: false, error: "timeout" } if the deadline is exceeded.

The runtime no longer exposes admin routes. Use the sibling zigttp-admin service to inspect runs and enqueue signals against the same durable directory:

  • GET /
  • GET /runs/:key
  • GET /contract
  • GET /_zigttp/durable/contract
  • GET /_zigttp/durable/runs
  • GET /_zigttp/durable/runs/:key
  • POST /_zigttp/durable/runs/:key/signals/:name

The durable section of contract.json also includes a workflow graph with workflowId, proofLevel, nodes, and edges.

cd ../zigttp-admin
deno task start --durable-dir ../zigttp/.zigttp-durable --contract ../zigttp/contract.json

curl http://127.0.0.1:8787/_zigttp/durable/contract
curl http://127.0.0.1:8787/_zigttp/durable/runs
curl -X POST \
  http://127.0.0.1:8787/_zigttp/durable/runs/order%3A42/signals/approved \
  -H 'content-type: application/json' \
  -d '{"approvedBy":"ops"}'

Proven Evolution (-Dprove)

Compare two handler versions and classify the upgrade:

zig build -Dhandler=handler-v2.ts -Dprove=old-contract.json:traces.jsonl

Two diff levels run against the old and new contracts. The surface diff compares I/O capabilities (env vars, egress hosts, cache namespaces, SQL query names, routes). The behavioral diff compares every execution path, matching by route and branching conditions, then classifying each as preserved, response-changed, removed, or added.

Property regressions carry severity. Losing retry_safe or injection_safe is critical. Losing deterministic or idempotent is a warning. Losing pure is informational.

The upgrade verdict combines these signals:

  • safe: Identical behavior, no property regressions
  • safe_with_additions: New paths or capabilities added, existing behavior preserved
  • breaking: Paths removed, responses changed, or critical property lost
  • needs_review: Structurally OK but warning-level regressions or significant coverage gaps

Output: proof.json (machine-readable certificate), proof-report.txt (human-readable), and upgrade-manifest.json (verdict with full breakdown).

Guard Composition (zigttp:compose)

Compose handlers with pre/post guards using the pipe operator:

import { guard } from "zigttp:compose";

const withAuth = guard((req: Request): Response | undefined => {
    if (req.headers["authorization"] === undefined)
        return Response.json({ error: "unauthorized" }, { status: 401 });
    return undefined;
});

const withCors = guard((res: Response): Response | undefined => {
    return Response.json(res.body, {
        status: res.status,
        headers: { "access-control-allow-origin": "*" }
    });
});

const handler = withAuth |> mainHandler |> withCors;

The parser desugars the pipe chain into a single flat function with sequential if-checks at compile time. Pre-guards receive the request and short-circuit on non-undefined return. Post-guards receive the response and can replace it. Exactly one non-guard handler is required.

External Enrichment Flags

Five optional build flags accept external JSON files for cross-referencing against compiler-proven contracts. These work with any code generator or hand-written files - no specific tooling required.

  • -Dmanifest=<path>: Cross-references a declared manifest (routes, SQL tables, env vars) against the handler contract. Errors on declared items missing from code, warns on undeclared items found in code. Emits manifest-alignment.json.
  • -Dexpect-properties=<path>: Verifies handler-derived properties (state_isolated, injection_safe, read_only, etc.) match external expectations. Build fails on mismatches.
  • -Ddata-labels=<path>: Merges externally declared data sensitivity labels (secret, credential, etc.) with the flow checker's heuristic labels. Violations of declared labels become build errors.
  • -Dfault-severity=<path>: Overrides fault severity classification at the route level. A route declared "critical" elevates all failable calls within it to critical severity for fault coverage diagnostics.
  • -Dreport=json: Emits a structured JSON build report aggregating verification, properties, fault coverage, flow analysis, manifest alignment, and property expectations into report.json.

All flags are optional and additive. Without them, nothing changes.

Precompiled Bytecode

Cold Start Performance:

Platform Cold Start Runtime Init Status
macOS (development) ~103ms 3ms Current
Linux (production) ~18-33ms (planned) 3ms Planned

macOS Performance (development environment):

  • Total cold start: ~103ms (2-3x faster than Deno)
  • Runtime initialization: 3ms
  • dyld overhead: 80-90ms (unavoidable on macOS, affects all binaries)
  • Acceptable for development; not suitable for latency-sensitive production

Linux Target (future production optimization):

  • Static linking with musl libc
  • Expected cold start: 18-33ms (70-85ms faster than macOS)
  • Zero dynamic dependencies
  • Requires fixing JIT cross-compilation issues

Embedded Bytecode Optimization (recommended for all platforms):

zig build -Doptimize=ReleaseFast -Dhandler=path/to/handler.js

Eliminates runtime parsing and compilation. Single binary, smaller container image, lower memory baseline.

Platform Strategy:

  • macOS: Development only (~100ms is acceptable)
  • Linux: Production target (sub-10ms goal via static linking)
  • Pre-fork/daemon: Alternative for sub-millisecond response times

See Performance for detailed profiling analysis and deployment patterns.

Compiler Revalidation

When revalidating against a newer Zig compiler, run:

zig version
zig build
zig build test
zig build test-zigts
zig build test-zruntime
bash scripts/test-examples.sh
ZTS_RUN_STRESS_TESTS=1 zig build test-zruntime
ZTS_RUN_FLAKY_IO_TESTS=1 zig build test -- --test-filter "parseRequest rejects long header"

Then update the validated compiler version in this README and docs/user-guide.md.

For faster local edit/build loops on ELF targets, keep incremental and new-linker usage opt-in:

zig build -fincremental --error-style=minimal_clear
zig build -Doptimize=Debug -fincremental --error-style=minimal_clear

zigttp does not enable these by default because Zig 0.16.0 still documents incremental compilation as experimental and the new ELF linker as not yet feature-complete.

Documentation

  • User Guide - Complete handler API reference, routing patterns, examples
  • Verification - Compile-time handler verification: checks, diagnostics, examples
  • Sound Mode - Type-directed analysis: arithmetic safety, + safety, tautology detection, truthiness, type-specialized codegen
  • Architecture - System design, runtime model, concurrency, project structure
  • Frontier - Strategic direction: core moat, next-frontier features, identity guardrails
  • JSX Guide - JSX/TSX usage and server-side rendering
  • TypeScript - Type stripping, compile-time evaluation
  • Performance - Benchmarks, cold starts, optimizations, concurrent I/O
  • Feature Detection - Unsupported feature detection matrix
  • API Reference - Zig embedding API, extending with native functions

JavaScript Subset

zigts implements ES5 with select ES6+ extensions:

Supported: Strict mode, let/const, arrow functions, template literals, destructuring, spread operator, for...of (arrays) with break/continue, optional chaining, nullish coalescing, typed arrays, exponentiation operator, compound assignments (+=, -=, *=, /=, %=, **=, bitwise), pipe operator (|>), array higher-order methods (.map(), .filter(), .reduce(), .find(), .findIndex(), .some(), .every(), .forEach()), Object.keys() / .values() / .entries(), Math extensions, modern string methods (replaceAll, trimStart/End), globalThis, range(), match expression (pattern matching).

Not Supported: Classes, async/await, Promises, var, while/do-while loops, this, new, try/catch, null, regular expressions, labeled break/continue, as/satisfies type assertions. All unsupported features are detected at parse time with helpful error messages suggesting alternatives. Use undefined as the sole absent-value sentinel.

See User Guide for full details.

License

MIT licensed.

Credits

  • zigts - Pure Zig JavaScript engine (part of this project)
  • Zig programming language
  • Codex & Claude

About

Native Zig TypeScript runtime that started as port of mquickjs to Zig... and... grew up to something bigger

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors