Documentation
The curated reference — enough to configure and operate riz without reading source. For exhaustive depth, the repository docs and the 900+ tests are the ground truth this page is pinned to.
Install
One static binary for Linux/macOS (x86_64 + aarch64). Runtime dependencies are per-language:
Bun for TypeScript, node, python3; Rust handlers are compiled binaries; WASM needs nothing (wasmtime is embedded).
$ curl -fsSL https://riz.dev/install | sh # release binary $ cargo install --git https://github.com/24X7/riz # from source $ riz doctor # pre-flight check
Scaffold a project
riz init always fetches templates from a git location — nothing is embedded in the binary.
Pass an official name (riz init --list), any owner/repo, a subdirectory of a repo, a git URL, or a local path —
so teams can publish and use their own templates. Set RIZ_TEMPLATE_REPO to resolve official names from a fork.
$ riz init typescript-todo my-app # official name → riz repo (full-stack demo) $ riz init owner/repo my-app # any GitHub repo $ riz init owner/repo/path/to/tmpl#v2 my-app # a subdirectory, at a ref/tag $ riz init https://host/o/r.git my-app # any git URL (incl. file://) $ riz init ./local/template my-app # a local path (offline)
Official templates: typescript-http · nodejs-http · python-http · rust-http · go-http · wasm-http ·
the three WebSocket variants · and typescript-todo — a full-stack sample (Bun API + React/Vite client served on one binary).
Flags: --ref <branch|tag>, --force (write into a non-empty dir), --git (init a repo + first commit).
riz.toml
One file declares the fleet. One [function.<name>] block = one warm process pool = N routes,
mirroring AWS Lambda + API Gateway v2. Omit routes and the function serves ANY /<name>.
[server] port = 3000 host = "127.0.0.1" [function.api] runtime = "bun" # bun | node | python | rust | go | wasm handler = "src/index.handler" # AWS file.export resolution timeout_ms = 30000 # handler kill + respawn deadline concurrency = 10 # pool size = max in-flight [[function.api.routes]] path = "/accounts/{id}" # AWS syntax: {param}, {proxy+} method = "GET"
Function fields
| key | default | what it does |
|---|---|---|
runtime | — | bun · node · python · rust · go · wasm (rust/go = unmodified official AWS Lambda binaries via the Runtime API) |
handler | — | AWS-style file.export; for rust/go/wasm, a path to the artifact |
timeout_ms | 30000 | handler deadline — exceeded = child killed + respawned |
integration_timeout_ms | 30000 | gateway wait — exceeded = 504 to the client |
concurrency | 1 | worker processes in the pool (≥ 1 enforced) |
env | off | per-function environment variables injected at spawn ([function.X.env]) — the standard way to hand a handler its DATABASE_URL / API keys; riz's own AWS_* vars win on conflict; process runtimes only (WASM keeps its deny-by-default WASI env) |
memory_mb / cpu_time_secs | off | opt-in RLIMIT_AS / RLIMIT_CPU caps |
allowed_paths | off | Linux Landlock filesystem allowlist (WASI preopens for wasm) |
authorizer | off | "fnName" (REQUEST), "none", or an inline JWT block |
guard_in / guard_out | off | .wasm policy modules — see Guards |
capabilities | off | broker grants (wasm only) — see Broker |
mcp | off | tool description / typed query params / body schema — see MCP |
MCP tools
Every function is a tool at /_riz/mcp (JSON-RPC 2.0 over Streamable HTTP, spec 2025-11-25,
negotiates down to 2024-11-05). Path params are typed + required from the route template automatically.
WebSocket functions are tools too: a tools/call with {message, timeout_ms?}
opens an ephemeral real session — $connect, the message as $default, every
@connections push collected as reply frames, then $disconnect.
The optional block sharpens the rest:
[function.search] runtime = "bun" handler = "./index.search" [function.search.mcp] description = "Search the catalog." [function.search.mcp.query.limit] type = "integer" # string | integer | number | boolean description = "Max results" required = true
- Validation: missing required params / type mismatches → JSON-RPC
-32602naming the parameter. - Streaming: POST with
Accept: text/event-stream→ SSE frames; GET opens the server channel;Mcp-Session-Idhonored; notification-only POSTs → 202. - Progress: send
params._meta.progressTokenon a slow call → specnotifications/progressframes until the result. - Auth:
Authorization: Bearerwhen a token is configured.
Guards
A guard is a wasm32-wasip1 module run as a sibling pool. guard_in sees every event before the
handler; guard_out sees every response before bytes leave. One module can serve both. Failures fail closed.
{"action":"allow"} # pass through
{"action":"allow", "event":{…}} # guard_in: mutated event
{"action":"allow", "response":{…}} # guard_out: replaced response
{"action":"deny", "statusCode":451, "body":"…"}
- Guard budget is 2s per call, non-configurable — policy must be fast.
- Per-guard timing appears in
/_riz/healthasfn::guard_in/fn::guard_outentries. - A guard that can't spawn is a startup error — a configured policy is never silently absent.
Capability broker
WASM-only in v1. Resources are declared once (credentials host-side via env names); grants reference them per function.
The guest calls pg_query(grant, request) via host imports — no sockets, no DSNs in guest memory.
| grant key | default | enforces |
|---|---|---|
type / resource | — | v1: "pg" → a declared [resources.pg.<name>] |
mode | read-write | read-only runs inside a genuine READ ONLY transaction |
max_inflight | 4 | concurrency cap — excess rejected (throttled), never queued |
rate_per_sec | ∞ | token-bucket rate limit |
call_timeout_ms | 1500 | per-call deadline — a stalled backend is cut, host unaffected |
max_request_bytes / max_response_bytes | 64 KiB / 1 MiB | payload caps both directions |
denied · throttled · timeout · too_large · bad_request · backend — a closed set, always JSON, never a trap.LLM gateway
Enable with a [gateway] block (see the gateway page for a full example).
Providers: openai, anthropic, ollama, mock. Routing by model prefix, then default_provider, then the fallback chain.
| endpoint | does |
|---|---|
POST /_riz/v1/chat/completions | OpenAI shape; SSE on stream: true |
POST /_riz/v1/embeddings | embeddings, same routing |
GET /_riz/v1/models | configured providers/models |
GET /_riz/v1/usage | cumulative cost + tokens per provider |
budget_usd breach → HTTP 412. Unknown models bill at the most expensive tier (the cap fails closed); ollama/* is known-free. All /_riz/v1/* routes are bearer-gated when a token is configured.
Endpoint surface
| endpoint | auth | does |
|---|---|---|
GET /health · GET /ready | open | liveness / readiness probes |
GET /_riz/health | open | per-function health, percentiles, guard timing |
GET /_riz/metrics | bearer | Prometheus text format |
GET /_riz/registry | bearer | function + route inventory |
POST /_riz/mcp | bearer | MCP JSON-RPC (SSE when negotiated) |
GET /_riz/connections · /_riz/connections/{id} | bearer | WebSocket @connections management |
/_riz/v1/* | bearer | the LLM gateway |
POST /deploy | deploy_key + CIDR allowlist | S3 hot-swap with health-check auto-rollback |
POST /cache/invalidate | bearer | evict cached responses by key or prefix |
"bearer" = gated when RIZ_AUTH_BEARER_TOKEN / [auth] bearer_token is set (constant-time compare); open for local dev when unset.
Scaling note (WebSockets): the connection registry behind @connections is
per-instance (in-memory). Running multiple replicas behind a load balancer requires sticky
sessions (session affinity — Cloud Run --session-affinity, ALB target-group
stickiness, nginx ip_hash); a push to a socket held by another replica returns
410 Gone, exactly as API Gateway does for a stale connection. Cross-replica
broadcast needs a shared backplane — a roadmap item, not in the binary today.
The full story is on the compare page.
Observability
riz exports traces as OTLP/HTTP-JSON to <endpoint>/v1/traces — spec-correct hex ids, a service.name = riz resource attribute — so any OTLP backend accepts them. There is exactly one export path: every backend below is the same POST with a different endpoint + headers. Independently, GET /_riz/metrics serves Prometheus text.
[telemetry] enabled = true endpoint = "http://localhost:4318" # any OTLP/HTTP collector or agent base URL [telemetry.headers] # optional — added verbatim to every export # x-honeycomb-team = "your-key"
| backend | endpoint | [telemetry.headers] |
|---|---|---|
| OTel Collector · Grafana Tempo · Jaeger | http://localhost:4318 | — |
| Datadog — Agent / Collector OTLP receiver | http://localhost:4318 | — (enable the agent's OTLP ingest on :4318) |
| Honeycomb | https://api.honeycomb.io | x-honeycomb-team |
| AWS X-Ray — via ADOT / OTel Collector | http://localhost:4318 | — |
The GA route to any vendor is an OTLP collector/agent — riz never speaks a vendor wire protocol (no StatsD, no X-Ray UDP). Datadog's agentless OTLP trace intake (a dd-api-key header straight to Datadog) is GA for metrics/logs but Preview for traces; use the Agent/Collector path until you have access. LLM-gateway spans carry OpenTelemetry GenAI attributes (gen_ai.usage.input_tokens / output_tokens, gen_ai.request.model, gen_ai.provider.name), so Datadog LLM Observability and similar classify them automatically. Copy-paste blocks for every backend: docs/integrations/observability.md.
CLI
| command | does |
|---|---|
riz run · riz --dev | boot headless · boot with the live Ratatui dashboard (--dev goes before any subcommand; run is the default) |
riz init <spec> [dir] [--ref r] [--git] | scaffold from git — an official name (--list), owner/repo[/subdir], a git URL, or a local path (never embedded) |
riz scaffold static [dir] [--wire] [--force] | generate llms.txt + .well-known/riz.json from your functions; --wire adds a [static] block |
riz validate · riz routes | config check · route table |
riz deploy <fn> <bucket> <key> | S3 hot-swap from the CLI |
riz mcp inspect [--url] [--bearer] | initialize + tools/list + typed params + SSE probe, one screen |
riz doctor | pre-flight: config, runtimes on PATH, handler files, port, MCP ping |
A2A agent
An [agent] block (requires [gateway]) turns the instance into an
agent2agent-protocol server:
an Agent Card at /.well-known/agent-card.json and JSON-RPC at POST /_riz/a2a
(SendMessage · SendStreamingMessage, SSE — live status + artifact events · GetTask · CancelTask; bearer-gated like the rest of /_riz/*).
A delegated task runs a server-side agent loop — the configured model reasons through the gateway with this
instance's own functions as tools (the same typed MCP tools, WebSocket sessions included), every call metered and budget-capped.
| key | default | what it does |
|---|---|---|
name / description | — | Agent Card identity |
model | — | gateway model the agent reasons with (anthropic/claude-…, mock, …) |
system_prompt | off | prepended to every task |
tools | all functions | allowlist of functions the agent may wield (also gates the card's skills) |
max_hops | 5 | agent-loop cap: one model → tools round trip = one hop |
task_timeout_ms | 60000 | how long SendMessage blocks before returning a working snapshot (poll GetTask) |
peers | off | [agent.peers] name→URL map — each peer's Agent Card is fetched and the peer becomes a delegate_to_<name> tool (riz-to-riz mesh; riz-a2a-hop header + max_hops stop delegation loops). Test any A2A server with riz a2a send <url> "message" |
Lifecycle
- Boot: one warm pool per function,
concurrencyworkers each, cold-started once. Every child gets the always-on safety profile (RLIMIT_CORE=0, NOFILE/FSIZE/NPROC caps, dies-with-host, no-new-privs). - Steady state: no per-request cold start. A child that crashes or hangs is detected by the liveness watcher and respawned; your route stays up.
- Change: edit
riz.tomlor handler source → debounced hot-reload swaps the pool.POST /deploypulls from S3, drains in-flight work over 30s, promotes atomically, and auto-rolls-back on a failed health check. - Shutdown: SIGTERM/SIGINT → graceful 30s drain, then children are killed as a group — nothing orphaned.
Static files
Point [static] at a directory and a riz instance serves it on the same binary and origin as your API — no second host, no CORS, no extra infra.
The compounding use is self-description: drop an llms.txt and .well-known/riz.json in that dir and a live instance answers
GET /llms.txt and /.well-known/riz.json, so an agent pointed at your host discovers the when-to-use card and the /_riz/mcp
endpoint with no separate marketing site. Colocating an SPA or landing page is the second use.
[static] dir = "./public" # required — served as the site root mount = "/" # URL prefix (default "/") spa_fallback = false # unknown HTML route → index.html (history-API SPAs) precompressed = false # serve file.br / file.gz when the client allows
- Functions and
/_riz/*always win. Static is the last GET/HEAD fallback before a 404 — it can never shadow an API or a system route. - Boring parts done right:
ETag+304,Range→206, correct content types (incl..wasm), immutable cache for hash-named assets andno-cachefor HTML, directory →index.html(never a listing). - Traversal, symlink escape, and dotfiles are rejected — except the agent surface under
/.well-known/*. - It is not a web server: no templating, SSR, reverse-proxying, or autoindex. For scale, front riz with a CDN.
Don't hand-write those discovery files — generate them. riz scaffold static reads your riz.toml and emits
public/llms.txt + public/.well-known/riz.json with every function rendered as a tool entry (name, runtime, routes,
description) matching exactly what /_riz/mcp advertises; --wire adds the [static] block for you. Re-run it
when your functions change and the instance's self-description stays in sync.