Introducing Fitz: a language where HTTP, Postgres, JWT, and WebSockets are part of the syntax
Source: Dev.to
Fitz is a new programming language built in Rust, with a gradually-typed compiler. The pitch: instead of stacking FastAPI + SQLAlchemy + python-jose + Celery + Pydantic + uvicorn + Alembic + typer on top of Python, the things they each solve live inside the language: HTTP routing, OpenAPI/AsyncAPI generation, async/await, JWT auth, password hashing, an ORM with a pure-Rust Postgres driver, schema migrations, WebSockets, cron, background jobs, a CLI builder, healthchecks, observability with OpenTelemetry, secrets as opaque types, and a fitz deploy orchestrator. One binary. Zero external deps for the core stack. Repo: github.com/Thegreekman76/fitz · Docs: thegreekman76.github.io/fitz 0
type User { id: Int, email: Str, name: Str, role: Str } type Credentials { email: Str, password: Str } type LoginResponse { token: Str }
let SECRET = “demo-secret-change-me-in-prod” let ADA_HASH = hash.password(“secret-ada-123”)
@auth_provider fn check_token(headers: Map) -> Result { let auth: Str = match headers.get(“authorization”) { Ok(v) => v, Err(_) => return Err(“missing Authorization header”), } let parts = auth.split(” ”) if (parts.len() != 2 or parts[0] != “Bearer”) { return Err(“expected ‘Bearer ’”) } let claims = jwt.decode(parts[1], SECRET)? return find_user(claims[“email”]) }
@post(“/login”) fn login(creds: Credentials) -> LoginResponse { let user: User = match find_user(creds.email) { Ok(u) => u, Err(_) => return 401 { “error”: “invalid credentials” }, } if (not hash.verify(creds.password, ADA_HASH)) { return 401 { “error”: “invalid credentials” } } let claims = { “email”: user.email, “role”: user.role } return LoginResponse { token: jwt.encode(claims, SECRET) } }
@authenticated @get(“/me”) fn me(user: User) -> User => user
@admin @get(“/admin/users”) fn admin_list(user: User) -> List { … }
Enter fullscreen mode
Exit fullscreen mode
What this code does, **without a single `import` or external dependency**:
- Starts an HTTP server on port 43928.
- Auto-generates OpenAPI 3.1 at `/openapi.json`.
- Auto-serves Scalar UI at `/docs` with a working "Authorize" button.
- Signs and verifies JWT tokens (HS256/384/512 supported).
- Hashes passwords with **Argon2id** (OWASP recommendation, not bcrypt).
- Statically validates that every `@authenticated`/`@admin` handler has an `@auth_provider` declared, that the provider returns the right `User` type, and that `@admin` handlers have a `role: Str` field on the `User`.
- Compiles to a single native binary with `fitz build`, with bit-for-bit parity against `fitz run`.
The auth, the hashing, the JWT, the OpenAPI with `bearerAuth` security scheme, the 401/403 responses — all of that is in the binary `fitz` itself. There's no `requirements.txt`, no `package.json`, no `Cargo.toml` for the user.
Why "first-class" matters
"First-class citizen" is one of those phrases that gets thrown around. Here's what I mean concretely.
In FastAPI, `@app.get("/users")` is a method on an object instance. The framework is a library you opt into. The router is a Python data structure. Authentication is a `Depends(...)`. None of those things are visible to the type checker as anything special — they're just function calls and decorators that happen to produce metadata.
In Fitz, `@get("/users")` is a **decorator the compiler understands**. The checker validates the path template, the parameter types against the path params, the body type, the return type. The OpenAPI generator inspects the AST directly — it doesn't introspect runtime objects, it doesn't need decorators that "register" themselves. The `User` you return in your handler is the same `User` that appears in the generated schema and in the Scalar UI.
This sounds like a small distinction until you live it for a week. Then you stop fighting "why does Pydantic disagree with SQLAlchemy about whether this field is optional" and you start writing endpoints.
The pieces
HTTP + OpenAPI + Scalar UI, all auto
type Post { id: Int, title: Str, body: Str, tags: List }
@get(“/posts”) fn list_posts() -> List { … }
@post(“/posts”) fn create_post(post: Post) -> Post { … }
Enter fullscreen mode
Exit fullscreen mode
That's all you need. `/openapi.json` and `/docs` (Scalar UI) appear automatically. Path params (`/posts/{id}`) are typed and coerced. JSON body deserialization checks for missing required fields, applies defaults, validates nullables, rejects extras. You can opt out with `@server(docs=false)`.
WebSockets, typed, with AsyncAPI auto-generated
type ChatMessage { from: Str, text: Str }
@server(43929, ws_heartbeat_secs=30) fn main() => 0
@authenticated @ws(“/chat”) async fn chat(conn: WsConn, user: User) { loop { let msg = match conn.recv() { Ok(m) => m, Err(_) => break, } conn.broadcast(ChatMessage { from: user.name, text: msg.text }) } }
Enter fullscreen mode
Exit fullscreen mode
Every frame is auto-marshalled to and from the declared type. Auth runs **before** the WebSocket upgrade — invalid token gets a 401 without ever opening the socket. Ping/pong heartbeat keeps the connection alive past Nginx's 60s default. `/asyncapi.json` is generated automatically (the event-driven sibling of OpenAPI). I don't know of another language that auto-generates AsyncAPI from typed source.
Background jobs and cron, no Redis required
@cron(”*/5 * * * *”) async fn cleanup_old_sessions() { db.exec(“DELETE FROM sessions WHERE expires_at User { let user = create_user(creds) spawn(send_welcome_email(user.email)) // fire-and-forget, typed Future return user }
Enter fullscreen mode
Exit fullscreen mode
No Celery. No Redis. No `celery worker -A app` next to your `uvicorn` process. The scheduler is in your binary. Suitable for 90% of services — when you outgrow it, you outgrow it for a reason, and that's a Fase 11+ problem.
A native ORM with a pure-Rust Postgres driver
This is the piece I'm most proud of, and the one that took the longest. Fitz has its own Postgres driver written in Rust — no `libpq`, no `tokio-postgres`, no `sqlx`. The wire protocol (v3.0), SCRAM-SHA-256 auth, prepared statements, the binary format for 11 OID types — all implemented from the RFC.
@table(“users”) type User { @primary id: Int, email: Str, name: Str, @has_many(“Post”, “user_id”) posts: List, }
@table(“posts”) type Post { @primary id: Int, user_id: Int, title: Str, body: Str, @belongs_to user: User?, }
@get(“/users”) async fn list_users(db: DbConn) -> List { return User.all(db).preload(“posts”).await }
@get(“/users/{id}”) async fn get_user(db: DbConn, id: Int) -> Result { return User.where(fn(u) => u.id == id).first(db).await }
@post(“/users”) async fn create_user(db: DbConn, user: User) -> User { return User.insert(db, user).await }
Enter fullscreen mode
Exit fullscreen mode
The closure inside `.where(...)` is **translated to parametrized SQL at compile time** — `fn(u) => u.id == id` becomes `WHERE id = $1`. Operators like `.is_in([...])`, `.like(...)`, `.ilike(...)`, `.contains(...)`, plus JSONB operators like `.has_key(...)`, `.contains_json(...)` all map to native Postgres operators. Eager loading with `.preload("posts")` issues a single batched query. Aggregates (`.sum`/`.avg`/`.min`/`.max`/`.count`) and `GROUP BY` are supported through a separate `Aggregated` type.
This compiles to native code via `fitz build`. The generated binary makes the same Postgres calls. Zero overhead at runtime for the SQL — it's already constant by the time the binary runs, comparable in performance to Diesel or sqlx.
Python interop when you do need it
from python import math, json
let radius = 5.0 let area: Float = math.pi * radius * radius
let parsed: Result> = match json.loads(”{“name”: “ada”}”) { Ok(d) => Ok(d), Err(e) => Err(“malformed JSON: {e}”), }
Enter fullscreen mode
Exit fullscreen mode
SQLAlchemy, NumPy, pandas, anything on PyPI — accessible from Fitz with `from python import ...`. The runtime embeds CPython via PyO3. Python exceptions become `Result::Err` automatically. Async Python (`asyncpg`, SQLAlchemy 2.x async) bridges to Fitz's `.await` transparently. You can even do `fitz build --bundle-python` to ship a binary with CPython embedded — no Python required on the destination machine.
This is intentional. Fitz isn't trying to replace Python's ecosystem — it's trying to give you a better language for the web layer while keeping the door open to everything Python has already built.
Async, finally without color
async fn fetch_user(id: Int) -> Result { … }
async fn main() { let user = fetch_user(42).await? print(“got {user.name}”) }
Enter fullscreen mode
Exit fullscreen mode
`async`/`await` is in the core, on a tokio runtime. The `?` operator works through `Result`. The type checker enforces that `?` only appears inside functions that return `Result`. Compiles to `async fn` + `.await` in Rust — same execution model as Rust async, same multi-threaded executor.
CLI builder — same language, command-line tools
Fitz isn't only for HTTP services. The same compiler ships a built-in CLI builder, no library needed:
@command(“greet”, desc=“Greet a person”) fn greet(name: Str, loud: Bool = false, count: Int = 1) -> Int { let n = count while n > 0 { if loud { print(“HELLO, {name}!”) } else { print(“hello, {name}”) } n = n - 1 } return 0 }
@command(“add”, desc=“Sum two numbers”) fn add(a: Int, b: Int) -> Int { print(“{a + b}”) return 0 }
Enter fullscreen mode
Exit fullscreen mode
$ ./mybin greet Ada —loud —count 3 HELLO, Ada! HELLO, Ada! HELLO, Ada!
$ ./mybin —help USAGE: mybin [ARGS] [OPTIONS] COMMANDS: greet Greet a person add Sum two numbers
Enter fullscreen mode
Exit fullscreen mode
Convention over decoration: params without defaults are positional args, params with defaults are flags. Bool with `default = false` becomes `--flag`, other types become `--flag `. Short flags auto-derive (`--loud` → `-l`) with conflict detection. Help auto-generated, exit codes POSIX standard. **Bit-for-bit parity** between `fitz run` (development) and `fitz build` (a self-contained binary you can drop into `/usr/local/bin`).
This is the same language. Same type checker. Same async/await. Same `Result` for errors. If your tool needs to hit the database, the ORM is there. If it needs HTTP, `@get`/`@post` are there. The line between "web service" and "CLI tool" stops being a stack decision.
Production-ready stack — from repo to production
This is what separates Fitz from "interesting prototype" languages. Real services need health checks, secrets, observability, and a way to ship. All of them are part of the language:
@server(43928) fn main() => 0
// Auto-mounted at GET /healthz and /readyz — Kubernetes-friendly. @healthz fn liveness() -> Bool => true
@readyz async fn readiness(db: DbConn) -> Bool { return match db.exec(“SELECT 1”).await { Ok() => true, Err() => false, } }
// Secret never leaks to logs, prints ”***” on Display. let db_url: Secret = secret(“DATABASE_URL”) let log_level: Str = config(“LOG_LEVEL”, “info”)
// Tracing + metrics with one decorator each. @trace(name=“process_order”) @metric(name=“orders”) async fn process(order: Order) -> Result { // process_order_duration_seconds (histogram) and orders_calls_total // (counter) populate automatically on drop. }
// Feature flags with two sources: fitz.toml [flags] + FITZ_FLAG_ env vars. @flag(“new-checkout”) @post(“/v2/checkout”) fn v2_checkout(body: Cart) -> Receipt { … }
Enter fullscreen mode
Exit fullscreen mode
Behind the scenes:
**HTTP access logs** auto-emit with `trace_id`/`span_id` propagated to every `log.info(...)` inside the handler.
**OpenTelemetry OTLP** export with one env var: `OTEL_EXPORTER_OTLP_ENDPOINT`. Spans flow to Jaeger/Tempo/Honeycomb. Without the env var, zero overhead, zero network calls.
**Prometheus `/metrics`** endpoint exposes counters and histograms — `@server(prometheus=true)` enables.
**`@flag` on HTTP/WS handlers** returns 404 when the flag is off — gate the hot path before middleware/auth.
Deploying:
Generate the Dockerfile + docker-compose.yml from the program shape.
fitz docker init
Build the binary, the Docker image, push to a registry.
fitz deploy docker —tag mycorp/api:v1
Or bring up locally with compose.
fitz deploy compose
Enter fullscreen mode
Exit fullscreen mode
`fitz docker init` reads your AST. If there's a `db.connect(...)`, it adds Postgres to the compose. If there's `@server(N)`, it sets `EXPOSE N`. If there's `@cron`, it adds `restart: unless-stopped`. If there's `from python import ...`, it picks `python:3.12-slim-bookworm` instead of distroless. **It generates what you'd write by hand**, you commit it, edit when you need to.
I'm not aware of another language where deployment is a language feature. It is here because every project I shipped in Python ended with two days of debugging Dockerfile gotchas.
What's the tooling like?
This is the part I underestimated when I started. A language without good tools is dead on arrival. Here's the current state:
**`fitz run`** — interpret the file directly. Fastest feedback loop.
**`fitz build`** — compile to a native binary via a generated Rust project. Bit-for-bit parity with `fitz run` is a hard requirement.
**`fitz check`** — type checker only, no execution.
**`fitz test`** — built-in test runner with `@test` decorator and `assert`, `assert_eq`, `assert_throws`. Cargo-style output.
**`fitz dev`** — hot reload. Watches `*.fitz` and `fitz.toml`, kills and respawns the child on change.
**`fitz fmt`** — opinionated formatter, zero config. Preserves your comments and blank lines.
**`fitz lint`** — 4 built-in lints with `// @allow()` suppression. Cargo-clippy-style output.
**`fitz repl`** — interactive REPL with multi-line support, `:type`, `:load`, persistent history.
**`fitz openapi`** — emit the OpenAPI schema without running the server.
**`fitz db diff`/`migrate`** — schema migration tooling. Diff the live DB against the `@table` types in your code, generate idempotent migrations, apply them with `fitz db migrate`. Same model as Alembic but with the types as source of truth.
**`fitz docker init`/`build`** — generate the Dockerfile + `.dockerignore` + `docker-compose.yml` from the program shape, then `docker build` wrapped.
**`fitz deploy docker`/`compose`** — thin wrapper to ship the image or bring up locally with one command.
**VSCode extension** — diagnostics + hover + go-to-definition + autocomplete + **signature help** + **format on save** + **bidirectional type inference** for callbacks, multi-platform distribution.
**`fitz new`** + **`fitz add`** + **`fitz remove`** + **`fitz update`** — package manager with `fitz.toml`, lockfile, path deps, git deps.
The LSP is real (`tower-lsp` under the hood). The formatter is real (your code round-trips through it). The test runner is real. The whole thing is dogfooded — I write Fitz code with the same VSCode extension I ship.
Being honest about state
This is a one-developer project. I started learning Rust to build it. I'm not going to pretend it's production-ready for everyone — here's what's true today (June 2026, release v0.15.0):
**What works end-to-end, with bit-for-bit `fitz run` ↔ `fitz build` parity:**
- HTTP server with `@get`/`@post`/`@put`/`@delete`, OpenAPI auto, Scalar UI.
- Middleware chain with `@middleware(fn)` + CORS built-in.
- JWT auth with `@auth_provider`/`@authenticated`/`@admin` + `@requires("custom_role")` for RBAC. Argon2id password hashing. Token blacklist over Postgres for logout/refresh.
- WebSockets with `WsConn`, AsyncAPI auto, heartbeat, auth pre-upgrade.
- Cron jobs with `@cron("expr")` (with retry, timezone, persistence, catch-up), background jobs with `@background` + `spawn(...)`.
- Postgres ORM with `@table`/`@primary`/`@column`/`@belongs_to`/`@has_many`, closure-to-SQL, eager loading, **transactions** (`db.transaction(fn)`), **schema migrations** (`fitz db diff`/`migrate`).
- TLS strict for Postgres (`sslmode=require`).
- Async/await on tokio.
- Python interop with `from python import ...`, including auto-bridging async.
**CLI builder** with `@command` — same language for CLI tools.
**Production stack**: `@healthz`/`@readyz`, `Secret`, `secret()`/`config()`, `@trace`/`@metric`, `@flag`, OpenTelemetry OTLP export, Prometheus `/metrics`, `fitz docker init/build`, `fitz deploy`.
- Package manager with path deps and git deps.
- Full tooling: LSP (with signature help, format on save, hover over params and bindings), fmt, test, dev, repl, lint.
**What's not in the box yet:**
- Frontend in `.fitz` (single-file components, SSR). Roadmap (Fase 11) — the most ambitious bet of the project. Not started.
- A public package registry. Path deps and git deps work today; the registry is on hold until there's real demand.
`fitz deploy` targets beyond `docker`/`compose` (no `fly`/`railway`/`k8s` wrapper yet — use the native CLIs).
- Interactive debugging in VSCode (Debug Adapter Protocol). Workarounds: `print`, REPL `:type`/`:env`, LSP diagnostics. Tracked as V6 in the backlog.
**What's stable**: ~3030 Rust unit tests + 13 LSP E2E + 360 compile E2E (smoke over every example in the guide) + ~140 more across other suites running in CI on every push. Clippy `-D warnings` clean.
How to try it
Install (Linux / macOS / WSL)
curl -fsSL https://raw.githubusercontent.com/Thegreekman76/fitz/main/install.sh | sh
Or grab a release binary from GitHub
https://github.com/Thegreekman76/fitz/releases
First server
fitz new my-api —http cd my-api fitz dev
Enter fullscreen mode
Exit fullscreen mode
Eight boilerplates ship in the repo under `boilerplates/`:
`api-simple` — minimal HTTP API.
`api-middleware-cors` — middleware chain + CORS configuration.
`api-postgres-fitz` — ORM + Postgres, Dockerized.
`api-postgres-python` — Postgres via Python/SQLAlchemy interop.
`api-websocket` — typed WebSocket chat.
`api-orm-full` — the full showcase: auth + ORM + WebSockets + cron + jobs.
`api-fullstack-postgres` — backend + minimal frontend in one binary.
`cli-tool` — CLI app with `@command` (no HTTP).
Each one runs with `docker compose up` or `fitz dev`. The README has the full matrix.
Why I built this
I live in El Chaltén, in Argentine Patagonia. The Fitz Roy is the granite tower that defines the skyline here. Borges wrote that we live in a country where the past is uncertain and only the future is real. I think that's true of programming languages too: the past is full of accumulated workarounds for missing language features, and the future is whatever you decide to build.
I've spent ten years writing API code in Python. I love FastAPI. But every time I start a new project, the first three hours are spent gluing libraries together to do the same thing I did last week. At some point the question becomes: what would a language look like that started from this set of needs in 2026, instead of growing them as patches on a language designed for shell scripting in 1991?
That's Fitz.
It's not done. I'm one person. It will get there.
**Repo**: [github.com/Thegreekman76/fitz](https://github.com/Thegreekman76/fitz)
**Docs and course**: [thegreekman76.github.io/fitz](https://thegreekman76.github.io/fitz/)
**Guide** (34 chapters): [thegreekman76.github.io/fitz/guide/](https://thegreekman76.github.io/fitz/guide/)
**Roadmap**: [docs/roadmap.md](https://github.com/Thegreekman76/fitz/blob/main/docs/roadmap.md)
**CHANGELOG**: [CHANGELOG.md](https://github.com/Thegreekman76/fitz/blob/main/CHANGELOG.md) — every release with detail.
**Issues**: [github.com/Thegreekman76/fitz/issues](https://github.com/Thegreekman76/fitz/issues)
*Next post in the series*: **"Build a URL shortener with Fitz: HTTP + Postgres + auth in 30 minutes"** — step by step from `fitz new` to a native binary running in Docker.