Zenith

The built-in web framework — no npm install, no Cargo.toml, no dependencies.

download Download page

Zenith (vn_modules/zenith.vn) and Meridian, the comptime ORM (vn_modules/db.vn) are Varian source files — not native C modules. Every file under vn_modules/ is automatically concatenated as a prelude when you run your program, so ZenithApp, new_app(), etc. are always in scope — no import needed.

auto_awesome Why Zenith?

Zenith's pitch is not "fastest raw throughput" (Go and Rust win there). It's everything a real web app needs, built in, secure by default, in one binary, with zero dependency tree.

inventory_2

Batteries included

SQLite, Postgres and Redis drivers, JWT auth, password hashing, request validation, HTML templates, signed sessions, OpenAPI docs — all built in. No node_modules, no supply-chain to audit.

flash_on

No imports, no wiring

Write new_app() and app.get(...) — no require, no plugin registration, no app.use(...) ceremony. Everything is automatically in scope.

Secure by default

Templates HTML-escape by default. Session cookies are HttpOnly; SameSite=Lax and JWT-signed. DB drivers use bound parameters. No opt-in required.

neurology

Comptime ORM

Query shapes are built at compile time — SQL strings are checked and baked into the binary before your program ever runs. bind() throws on param count mismatch.

description

OpenAPI for free

The summary you pass to each route feeds a generated OpenAPI spec. No decorator soup, no separate spec file to keep in sync.

package_2

Deploys as one binary

vn build --release produces a single hardened executable. No runtime to install, no lockfile to reconcile, no "works on my machine."

track_changes

Multi-core out the box

Clusters across threads with independent VMs, SIMD parsing, writev single-syscall responses, and a per-request arena that skips GC entirely.

Python escape hatch

Anything not built in (S3, niche SDKs) is one python.run() call away. "Batteries included" never becomes "stuck when you need something exotic."

play_arrow Quick start

Create a server, register a route, start listening. That's it.

server.vn
let app = new_app()
app.get("/", |req| {
    return Response { status: 200, body: "Welcome to Zenith!", content_type: "text/plain" }
}, "Root endpoint")
app.listen(8080)
terminal
Run it: ./vn run server.vn — then open http://localhost:8080

construction Building an app

A more complete example with middleware, route params, CRUD operations, and JSON responses:

app.vn
struct User { id: string, name: string }
let users = [User { id: "1", name: "Alice" }, User { id: "2", name: "Bob" }]

let app = new_app()

// Logging middleware
app.add_middleware(|req, next| {
    print("saw:", req.method, req.path)
    return next(req)
})

// Routes
app.get("/", |req| {
    return Response { status: 200, body: "Welcome!" }
}, "Root")

app.get("/api/users/:id", |req| {
    for i in 0..users.len() {
        if users[i].id == req.params.id {
            return Response { status: 200, body: json_encode(users[i]) }
        }
    }
    return Response { status: 404, body: "Not found" }
}, "Get user")

app.listen(8080)

new_app() returns an independent ZenithApp struct — you can run several apps with separate route tables in the same program. Route params (:id) are available via req.params.id. The third argument to app.get() is a summary string used only for auto-generated OpenAPI docs.

signpost_2 Routing & HTTP methods

Zenith supports all standard HTTP methods. Routes are stored in a radix/segment trie (RadixNode in zenith.vn) keyed on METHOD + path. A lookup costs one trie descent per path segment — not scanning every registered route.

routes.vn
let app = new_app()

// GET, POST, PUT, DELETE — all standard methods supported
app.get("/items", |req| { /* list */ }, "List items")
app.post("/items", |req| { /* create */ }, "Create item")
app.put("/items/:id", |req| { /* update */ }, "Update item")
app.delete("/items/:id", |req| { /* delete */ }, "Delete item")

// Route groups with shared prefix and middleware
let admin = app.group("/admin")
admin.add_middleware(auth_middleware)
admin.get("/dashboard", |req| { /* admin only */ })

app.listen(8080)

Route params & query strings

params.vn
// Path params: /users/42 → req.params.id == "42"
app.get("/users/:id", |req| {
    let id = req.params.id
    let page = query(req, "page", "1")  // ?page=2 → "2"
    return Response { status: 200, body: "User " + id + ", page " + page }
})

With the per-request struct arena and computed-goto bytecode dispatch, Zenith achieves ~10,500 req/s single-process on a plaintext benchmark — a ~40% improvement over base GC-heap allocation.

layers Middleware

Each middleware is a closure |req, next| { ... return next(req) } that wraps the next handler in the chain. There's no global state — the whole chain is passed explicitly through _run_middleware, so multiple app instances don't interfere.

middleware.vn
// Custom logging middleware
app.add_middleware(|req, next| {
    print(req.method, req.path)
    return next(req)
})

// Timing middleware
app.add_middleware(|req, next| {
    let start = time_now()
    let resp = next(req)
    print("took", time_now() - start, "ms")
    return resp
})

// Security: CORS + rate limiting (from shield.vn)
app.add_middleware(cors(["*"], ["GET", "POST"], ["Content-Type"]))
app.add_middleware(rate_limit(100, 60000))  // 100 req/min

Ready-made middleware from vn_modules/shield.vn:

  • cors(origins, methods, headers) — CORS headers
  • csrf() — CSRF token validation
  • rate_limit(max_reqs, window_ms) — in-memory rate limiter
  • rate_limit_redis(conn, max_reqs, window_seconds) — Redis-backed rate limiter

error Error handling

Catch uncaught errors from any middleware or route handler with app.on_error():

errors.vn
// Global error handler — catches all uncaught errors
app.on_error(|err, req| {
    print("ERROR:", err.kind, err.message)
    return Response {
        status: 500,
        body: json_encode({
            error: err.message,
            kind: err.kind,
            hint: err.hint
        }),
        content_type: "application/json"
    }
})

// Without a custom handler, returns clean JSON 500 that never leaks internals

swap_horiz Request & response helpers

Reading from the request

request.vn
query(req, "page", "1")       // ?page=2 → "2" (with default)
query_params(req)                // all query params as a struct
form(req, "email", "")         // form body field with default
form_params(req)                 // all form fields as a struct
cookie(req, "sid", "")        // single cookie by name
cookies(req)                     // all cookies as a struct

Building responses

response.vn
redirect("/login")                       // 302 redirect
redirect_with("/login", 301)             // 301 permanent redirect
json_response(value, 200)                // JSON response
html_response("<h1>OK</h1>")              // HTML response
with_header(resp, "X-Trace", id)        // add header to response
set_cookie(resp, "sid", v, null)       // Set-Cookie: HttpOnly; SameSite=Lax
clear_cookie(resp, "sid")               // expire cookie

key Signed sessions (JWT-backed)

sessions.vn
let secret = "my-signing-secret"

// Store data in a signed session cookie
let resp = session_set(resp, { user_id: 42, role: "admin" }, secret, null)

// Read it back — returns null if absent or forged
let data = session_get(req, secret)
if data != null { print(data.user_id) }

// Log out
let resp = session_clear(resp)

A forged/absent secret yields null — tamper-evident by design.

web HTML templates

render(template, ctx) renders an ERB-style template against a context struct. render_response(template, ctx) wraps the result in a text/html Response.

templates.vn
render("Hi <%= name %>", ctx)                         // HTML-escaped
render("<%- raw_html %>", ctx)                         // raw (trusted only)
render("<% for x in xs %>[<%= x %>]<% endfor %>", ctx)    // loop

// Full page render as response
app.get("/profile", |req| {
    return render_response("<h1>Welcome, <%= name %></h1>", { name: "Ada" })
})
DelimiterDescription
<%= expr %>check_circle Interpolate, HTML-escaped (safe by default)
<%- expr %>Interpolate raw/unescaped (trusted content only)
<% if cond %>...<% endif %>Conditional block
<% for x in items %>...<% endfor %>Loop over array

folder Serving static files

static.vn
let app = new_app()

// Serve a directory at a route prefix
app.serve_static("/assets", "./public")

// Now /assets/style.css serves ./public/style.css
app.listen(8080)

serve_static(route_prefix, directory) maps URL paths to files on disk. Directory traversal is blocked. Missing files return 404 — no framework internals exposed.

fact_check Request validation

Declarative decorators on struct fields — no Zod, no Joi, no JSON Schema:

validation.vn
// Validation decorators go directly on struct fields
struct SignupInput {
    @is_email email: string,
    @min_len(8) password: string,
    @is_uuid invite_code: string,
    @is_url website: string
}

// Using validated input in a handler
app.post("/signup", |req| {
    let input = form_params(req)
    // If input is invalid, it throws automatically
    // If it passes, all fields are validated and sanitized
    return Response { status: 201, body: "Created" }
})

science Testing

Test route behavior directly with app.handle(req) — no real HTTP socket involved:

test.vn
fn fake_req(method, path) {
    return http.create_struct(
        ["method", "path", "body", "json"],
        [method, path, "", null]
    )
}

// Test a route directly — no socket, no server needed
let resp = app.handle(fake_req("GET", "/api/users/2"))
print(resp.status, resp.body)

// Assertions in tests
assert(resp.status == 200)
assert(resp.body.contains("Bob"))

app.handle(req) runs routing + middleware + handler synchronously. app.listen(port) wraps app.handle in http.serve(port, |req| { return app.handle(req) }).

description OpenAPI docs

docs.vn
app.enable_docs("/docs")

Adds two routes: GET /openapi.json (the spec, built from real structs and serialized via json_encode) and GET /docs (Swagger UI page). Only routes registered before enable_docs() with a non-empty summary appear in the spec.

wifi WebSockets

ws.vn
app.ws("/live", |ws| {
    // Handle WebSocket messages
    let msg = ws.read()
    if msg != "" {
        print("received:", msg)
        ws.write("Echo: " + msg)
    }
})

Zenith handles the WebSocket upgrade (RFC 6455) automatically when you register a ws(path, handler) route. The handler receives a WebSocket struct with read() and write() methods. Server-sent events (SSE) are also supported via the standard response streaming API.

database Meridian: the comptime ORM

Meridian is Varian's built-in ORM. In astronomy the meridian is the coordinate line passing through the Zenith — just as a meridian maps the sky, Meridian maps your data with strict, compile-time boundaries. The query builder in vn_modules/db.vn separates shape (table, fields, WHERE clauses) from values (the actual data). Since the shape of a query is almost always static, it can run inside comptime { } — the SQL string is built and checked at compile time, then baked into the binary at zero runtime cost.

orm.vn
let compiled = comptime {
    select("users")
        .fields(["id", "name", "email"])
        .where("id", "=")
        .where("status", "=")
        .limit(10)
        .build()
}
// compiled.sql == "SELECT id, name, email FROM users WHERE id = ? AND status = ? LIMIT 10"

let conn = sqlite.connect(":memory:")
let bound = bind(compiled, [1, "active"])   // throws on count mismatch
let rows = run_sqlite(bound, conn)

// Without comptime (for dynamic shapes):
let q = select("users").fields(["id", "name"]).build()  // works at runtime too

Query builder API

MethodDescription
select(table)play_arrow Start building a SELECT query
.fields(["id", "name"])Select specific columns
.where(field, op)Add WHERE clause (value bound later via bind)
.limit(n) / .offset(n)Pagination
.paginate(page, per_page)Page-based pagination helper
.cursor_paginate(field, cursor, limit)Cursor-based pagination
.order_by(field, dir)Sort results
.use_dialect("postgres")Switch to $1, $2 placeholders
.find_by_key(field)Shorthand for .where(field, "=").limit(1)
.build()Produce CompiledQuery { sql, param_count }

bind(compiled, params) attaches runtime values and validates count. run_sqlite(bound, conn) / run_postgres(bound, conn) execute.

migration Database migrations

Meridian includes a migration system for versioning your database schema. Create a migrator, register up/down SQL, and apply or roll back:

migrations.vn
let conn = sqlite.connect("app.db")
let m = migration.new_migrator(conn)

// Register a migration with up and down SQL
m.register("create_users",
    "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
    "DROP TABLE IF EXISTS users")

// Register an up-only migration
m.register_up("add_index",
    "CREATE INDEX idx_users_name ON users(name)")

// Apply all pending migrations
m.up()

// Roll back the last migration
m.down(1)

// Check migration status
let pending = m.pending()
let applied = m.applied()

register() stores both up and down SQL; register_up() is for forwards-only changes like adding indexes. m.up() runs all pending migrations in order; m.down(n) rolls back the last n applied migrations. Use m.pending() and m.applied() to inspect migration state.

mail Sending email

email.vn
smtp.send({
    to: "user@example.com",
    from: "noreply@myapp.com",
    subject: "Welcome!",
    body: "Thanks for signing up."
})

SMTP is built into the runtime — no separate mailer library, no API key, no third-party service required. The smtp.send function blocks header injection and validates addresses.

web Lumen integration

Lumen is the server-driven UI framework built on top of Zenith. A Lumen component renders HTML on the server (via Zenith routes), and a tiny embedded JS runtime forwards browser events over a WebSocket so the server re-renders and morphs the DOM.

live.vn
lumen_component(state_fn, render_fn, handler_names, handler_fns)
lumen_mount(app, "/counter", counter)
  • GET /counter — serves initial HTML + client JS shell
  • GET /counter/live — WebSocket upgrade
  • Events flow as JSON: {"t":"event","h":"handler","v":"value"}{"t":"html","html":"..."}

code Extending via Python bridge

Anything not built in (S3, niche SDKs) is one python.run() call away:

extend.vn
let s3 = python.run("boto3", "client", ["s3"])
let buckets = s3.list_buckets()

// Or generate an image with Pillow
let img = python.run("PIL.Image", "new", ["RGB", [100, 100], [255, 0, 0]])