Orva

Documentation

Orva runs JavaScript, TypeScript and Python functions inside nsjail sandboxes on hardware you own, and serves them over HTTP. This page is enough to deploy something and operate it. The exhaustive reference is in the repository.

Getting started

Docker Compose is the shortest path. It publishes the dashboard on host port 3000, which maps to 8443 inside the container.

bash
$ curl -fsSL https://raw.githubusercontent.com/Harsh-2002/Orva/main/docker-compose.yml -o docker-compose.yml
$ docker compose up -d
# http://localhost:3000

nsjail is required on Linux. The server starts without it, but every invocation fails until it is installed at /usr/local/bin/nsjail. That path is hardcoded: PATH is never consulted and there is no environment override. The Docker image ships it.

The current installer verifies a real nsjail Node spawn before it reports success: inside the running container for Docker, and as the unprivileged orva service account on a VM. On a host that blocks user-namespace setup it verifies nsjail's file-capability fallback; if neither mode works it stops before starting a server that could not run functions. Bare-metal installs use a statically linked nsjail asset, so Fedora, Rocky, Arch, Alpine, Debian and Ubuntu do not depend on matching one distribution's protobuf or libnl ABI.

State is a single directory, /var/lib/orva: one SQLite database in WAL mode and a tree of function versions. There is no external database, no control plane and no telemetry.

Handler contract

There are two runtimes, node and python, both latest-stable only. TypeScript is a first-class path on node: it compiles at build time and runs as JavaScript.

Node.js

handler.js
exports.handler = async (event) => {
  const body = event.body ? JSON.parse(event.body) : {}

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: { hello: body.name || 'world' },
  }
}

Python

handler.py
import json


def handler(event):
    raw = event.get("body") or ""
    body = json.loads(raw) if raw else {}

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": {"hello": body.get("name", "world")},
    }

The body is always a raw string. The platform never parses it, whatever the Content-Type. Call JSON.parse or json.loads yourself, and guard the empty case. Treating it as an already-parsed object is the single most common way a first handler returns a 500.

Event and response

The event carries method, path, headers and body. Node additionally gets query, because its adapter parses it out of the path; Python handlers split event["path"] themselves.

Return { statusCode, headers, body }. A non-string body is JSON-encoded by the adapter. Environment variables and secrets arrive in process.env and os.environ.

Invoking

Every function answers at /fn/<function_id>. Friendlier paths are attached separately as custom routes, from the dashboard, the API, the set_route MCP tool or orva routes set.

bash
$ curl -X POST https://orva.example.com/fn/<function_id> \
    -H 'Content-Type: application/json' \
    -d '{"name": "Orva"}'

These prefixes are reserved and cannot be claimed by a custom route: /api/ /auth/ /fn/ /mcp/ /web/ /webhook/ /_orva/.

Function settings

Each function carries its own runtime, resource budget and network posture. The first five of these are spawn-scoped: a warm worker's environment is fixed when it spawns, so changing them takes effect on the next cold start rather than immediately.

runtime
node or python.
memory_mb / cpus
Enforced by cgroup v2 on the sandbox, not advisory. If the host does not delegate cgroup controllers, Orva keeps the sandbox running with an rlimit-only fallback and surfaces a persistent warning in system health and Settings; that fallback is not a substitute for the declared CPU, pid and hard-memory limits.
timeout_ms
Also surfaced to the handler as getRemainingTimeInMillis() and get_remaining_time_in_millis().
network_mode
none by default: an isolated network namespace with loopback only. egress opts into outbound traffic and adds roughly 5 ms to a cold start. Without it, any outbound call fails ENETUNREACH.
auth_mode
public is the default and matches Cloudflare Workers and Vercel Functions. signed reads its key from the function secret ORVA_SIGNING_SECRET.
max_concurrency
Caps in-flight invocations per function, so a runaway handler cannot exhaust whatever it calls downstream. 0 means unlimited.

The SDK

The orva package is preinstalled in every sandbox. There is nothing to add to a manifest and no network call to fetch it. Note that anything reaching outside the sandbox still needs network_mode: egress.

KV

A per-function JSON namespace on SQLite, with optional TTL. Batches are atomic: a validation or storage failure rolls the whole batch back. The Node SDK is promise-based; the Python SDK is synchronous.

Node.js
const { kv } = require('orva')

exports.handler = async (event) => {
  // Omit ttlSeconds to keep expiry, 0 clears it.
  await kv.put('user:42', { name: 'Ada', tier: 'pro' }, { ttlSeconds: 3600 })

  const user = await kv.get('user:42', null)
  const pages = await kv.list({ prefix: 'page:', limit: 50 })
  await kv.delete('user:42')

  return { statusCode: 200, body: JSON.stringify(user) }
}
Python
from orva import kv


def handler(event):
    kv.put("user:42", {"name": "Ada", "tier": "pro"}, ttl_seconds=3600)

    user = kv.get("user:42", None)
    pages = kv.list(prefix="page:", limit=50)
    kv.delete("user:42")

    return {"statusCode": 200, "body": user}

Function to function

invoke() calls another function on the same instance, with call-depth limits so a cycle cannot spend the host. invoke_stream() streams the response through instead of buffering it.

Background jobs

jobs.enqueue() moves work off the request path. Pass an idempotency key so a retry does not run it twice.

Logs and spans

log writes structured lines that land in the invocation log, and trace opens custom spans that appear in the trace view. The import is log, not logger.

Traces started by Orva keep the tr_ plus 32-hex format. The leading 12 hex characters encode Unix milliseconds and the remaining 20 are cryptographically random; incoming W3C trace IDs are preserved. A trace ID is for correlation, not authentication.

Python
from orva import trace, log


def handler(event):
    with trace.span("fetch-user"):
        log.info("looking up", user_id=42)

    return {"statusCode": 200, "body": "ok"}

Webhooks

Deliveries are signed, and each runtime ships a verification helper. The x-orva-* header namespace is server-set and stripped from inbound requests, so a caller cannot forge one.

CLI

A single static binary with no CGO, so it runs on a bare Alpine image. It reads ~/.orva/config.yaml for its endpoint and API key.

bash
# Key on stdin: the command line is visible via ps.
$ printf %s "$ORVA_KEY" | orva login --endpoint https://orva.example.com --api-key -

$ orva system health
$ orva deploy ./my-fn --name my-fn --runtime node
$ orva invoke my-fn --body '{"hello":"world"}'
$ orva logs my-fn --follow

Beyond those, the surface covers functions, deployments, rollback, diff, kv, secrets, cron, jobs, keys, traces, executions, activity, pool, routes, channels, firewall, dns, backup, chat and upgrade. Run orva docs for the full reference, which is embedded in the binary.

Server config

The server is configured by environment variables only. There is no server config file, and the defaults work without extra configuration.

Variable Default Purpose
ORVA_DATA_DIR ~/.orva from source; /var/lib/orva when packaged Persistent database, function code, runtime rootfs, and instance data.
ORVA_HOST 0.0.0.0 HTTP bind address. Use 127.0.0.1 behind a local reverse proxy.
ORVA_PORT 8443 Plain-HTTP listen port.
ORVA_WRITE_TIMEOUT_SEC 60 Buffered-response write timeout in seconds.
ORVA_MAX_BODY_BYTES 6291456 JSON API request-body limit; deploy and restore uploads have separate limits.
ORVA_CORS_ORIGINS * Comma-separated browser and MCP Origin allow-list.
ORVA_SECCOMP_POLICY default Sandbox policy: default, strict, permissive, or disabled.
ORVA_LOG_LEVEL info Logging level: debug, info, warn, or error.
ORVA_SECURE_COOKIES false Force secure cookies when Orva cannot observe TLS or X-Forwarded-Proto.
ORVA_TRUSTED_PROXY false Trust proxy client-IP headers; enable only behind a proxy that rewrites them.
ORVA_SESSION_DAYS 7 Session-cookie lifetime in days.
ORVA_PPROF_ADDR unset Optional loopback-only Go diagnostics listener, such as 127.0.0.1:6060.
ORVA_IMAGE image-stamped; unset on bare metal Image identity reported by health and Settings.
ORVA_DISABLE_USERNS installer-selected; 0 in Docker 0 uses user namespaces; 1 uses the verified capability fallback.
ORVA_CGROUPV2_MOUNT auto-detected Delegated cgroup v2 subtree for CPU, memory, and process limits.
ORVA_INTERNAL_API_BASE auto-detected Internal SDK base URL; override only if sandbox-to-host routing detection is wrong.

Runtime-control variables are normally selected by the installer or container. Read their security implications and validation rules in CONFIG.md.

Security model

Between a handler and the host kernel there are five layers:

what is between user code and the host
host kernel
  └─ docker container "orva"
      └─ orvad (Go server)
          └─ nsjail process         unshare(CLONE_NEWUSER) drops effective caps
              └─ user namespace
                  ├─ chroot to runtime rootfs      read-only
                  ├─ /code bind-mount              read-only, function-private
                  ├─ tmpfs /tmp                    private, wiped on worker exit
                  ├─ cgroup v2                     memory + CPU + pids
                  ├─ seccomp filter                ~150 syscalls blocked
                  └─ user code (node / python)

The gaps between those layers are enforced by the Linux kernel, not by Orva code. Orva configures the boundaries and trusts the kernel to hold them.

What is explicitly out of scope

Kernel-level zero-days and side-channel attacks. No seccomp filter is bulletproof, and the threat model says so rather than implying otherwise. Orva is also single-tenant: every function on an instance shares one host and one trust boundary, so it is not built to run code from people you do not trust.

Egress

Egress policy is per sandbox and fail-closed. The blocklist compiles into that sandbox's own network configuration, which the spawn refuses to start without. No host firewall table is ever created, so nothing Orva does can outlive the process that needed it.

Credentials

The SDK credential handed to a sandbox is bound to that worker and to a single spawn. It is released when the sandbox is reaped, so a redeploy genuinely invalidates a leaked copy rather than merely appearing to. SECURITY.md sets out each credential class and exactly what revoking it reaches.

MCP

Orva exposes 73 tools over Model Context Protocol, so an agent can operate the instance the same way you do. A read-only key reaches 28 of them. Authentication is OAuth 2.1 or a static bearer token.

Claude Code
$ claude mcp add --transport http orva https://orva.example.com/mcp/ \
    --header "Authorization: Bearer $ORVA_KEY"

Config snippets for Cursor, VS Code, Codex CLI, OpenCode, Zed, Windsurf, Claude Desktop and ChatGPT are in the reference. Agent channels let you bundle a set of functions and expose them as tools in their own right.

Troubleshooting

Every invocation fails, the server is up
nsjail is missing. It must exist at /usr/local/bin/nsjail; that path is hardcoded and PATH is not consulted.
An outbound call fails with ENETUNREACH
The function is on network_mode: none, which is the default. Turn on "Allow outbound network" in its configuration.
The handler returns 500 on a valid JSON POST
event.body is a raw string and has to be parsed. It is never an object, whatever the Content-Type says.
A config change did not take effect
Runtime, memory, CPU, timeout and network mode are baked in when a worker spawns. A warm worker keeps the old environment until it is replaced.
Sandboxes will not start under Docker
On the default runc runtime the container needs --pid host, --cgroupns host and --cap-add SYS_ADMIN, plus a writable cgroup mount, to construct sandboxes at all.
Every function crashes with setgroups or user-namespace stderr
Do not run the daemon as root. Re-run the current bare-metal installer: it tests the shipped nsjail binary as orva, then persists its file-capability fallback only if that fallback starts successfully. Check orva system health for sandbox.user_namespace, sandbox.resource_limits and warnings after installation.