Zenith
The built-in web framework — no npm install, no Cargo.toml, no dependencies.
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.
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.
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.
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.
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.
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."
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.
let app = new_app() app.get("/", |req| { return Response { status: 200, body: "Welcome to Zenith!", content_type: "text/plain" } }, "Root endpoint") app.listen(8080)
./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:
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.
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
// 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.
// 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 headerscsrf()— CSRF token validationrate_limit(max_reqs, window_ms)— in-memory rate limiterrate_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():
// 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
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
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)
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.
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" }) })
| Delimiter | Description |
|---|---|
<%= 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
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 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:
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
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
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.
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
| Method | Description |
|---|---|
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:
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
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.
lumen_component(state_fn, render_fn, handler_names, handler_fns) lumen_mount(app, "/counter", counter)
GET /counter— serves initial HTML + client JS shellGET /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:
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]])