Hosting
Host sites, functions and machines on network nodes: static sites over iroh, sandboxed wasi:http functions that are deny-by-default, and microVM servers.
Tenzro nodes can serve your applications. There are three kinds of workload, and they share one naming and ingress layer, so a hostname can point at any of them.
| Workload | What it is | Runs as |
|---|---|---|
| Site | A build-output directory: HTML, JavaScript, assets | Content-addressed files routed by a signed manifest |
| Function | A wasi:http WebAssembly component | A per-request sandbox with no ambient authority |
| Machine | An unmodified long-running server | A process in a hardware-virtualised microVM |
Every workload is owned by a DID, and every change to it is signed by that DID.
Sites
A site is a manifest: an owner DID, a map from paths to content-addressed files, an index route, an optional not-found route and a single-page-app flag. Files live as blobs on the iroh data plane. A request path only ever looks up the route map; it never touches a filesystem.
Requests resolve in order: an exact route, then (for single-page apps) the index route with status 200 so the client router takes over, then the not-found route with 404. A missing asset with a file extension returns 404 directly, so a missing bundle is never masked as the index page.
# Build your app, then publish the output directory
tenzro site deploy --name my-app --owner-did did:tenzro:human:... \
--dir ./dist --did-envelope <hex> --rpc https://rpc.tenzro.xyz
tenzro site list
tenzro site get --site-id <site_id>
tenzro site remove --site-id <site_id> --owner-did did:tenzro:human:... --did-envelope <hex>Single-page-app layout is detected automatically; override it with --spa or --no-spa.
Functions
A function is one WebAssembly component that exports the wasi:http/incoming-handler world. It compiles from Rust, TypeScript, Go, Python or any language with a WASI 0.2 target. The node compiles it once and runs it fresh for each request, with deterministic fuel metering and a wall-clock deadline. A function keeps no state between requests; keep state in a granted storage mount, an external service or a database.
Deny-by-default
A function starts with no filesystem, no network, no environment and no host methods. It gets only what its capability grant names:
{
"storage": [{ "mount": "/data", "read_only": false }],
"network": [{ "host": "api.example.com", "port": 443 }],
"env": { "LOG_LEVEL": "info" },
"host_methods": ["chat"]
}The grant is a request, not an entitlement. The serving node intersects it with its operator's hosting policy, and the function runs with the smaller of the two. In addition:
- Host access. Storage mounts are confined to the function's own sandboxed directory. A function cannot reach the host's files.
- Egress. Outbound connections go only to the hosts and ports in the grant, and never to loopback, link-local, private-range or cloud metadata addresses.
- Secrets. A function sees only the environment values its grant supplies. The host's own environment, credentials and tokens are never passed through.
tenzro function deploy --name my-fn --owner-did did:tenzro:human:... \
--wasm ./target/wasm32-wasip2/release/my_fn.wasm \
--capabilities ./caps.json \
--did-envelope <hex> --rpc https://rpc.tenzro.xyz
tenzro function get --id <id>
tenzro function list
tenzro function remove --id <id> --owner-did did:tenzro:human:... --did-envelope <hex>Omit --capabilities and the function runs with no authority at all.
Machines
A machine is an ordinary server, in Node, Python, Rust, Go or anything else, that listens on a local port. It runs inside a Firecracker microVM, so it keeps a resident process: persistent connections, in-memory caches and background loops survive between requests. Declare the port as internal_port; the ingress forwards each request to it as plain HTTP, so an unmodified server works without code changes.
Each microVM runs under a jailer as an unprivileged account, in its own cgroup, with a syscall filter. Set --tee-required to place the machine only on nodes that run it inside a TEE.
Environment secrets are sealed before they are sent. The CLI fetches the assigned node's X25519 sealing key, encrypts each value to it, and sends only ciphertext. The node unseals values in memory when it starts the microVM and never stores them in the clear.
# --env is a JSON file of secrets, sealed to the node before sending
tenzro machine deploy --name my-server --owner-did did:tenzro:human:... \
--image ./rootfs.ext4 --internal-port 8080 \
--vcpus 2 --mem-mib 1024 --disk-mib 4096 \
--env ./secrets.json \
--did-envelope <hex> --rpc https://rpc.tenzro.xyz
tenzro machine status --id <id>
tenzro machine remove --id <id> --owner-did did:tenzro:human:... --did-envelope <hex>A node without the microVM supervisor can hold machine metadata but answers machine requests with HTTP 501; placement sends machines only to capable nodes.
Names and domains
Each operator serves apps under its own app domain; there is no single network-wide host. Point a hostname under the hosting operator's app domain at a site, function or machine with an alias. Setting an alias requires proof that you control the owner DID and own the target; re-pointing an existing alias requires owning it too.
tenzro site set-alias --hostname my-app.<operator-app-domain> --site-id <id> \
--owner-did did:tenzro:human:... --did-envelope <hex>To use your own domain, claim it and publish the DNS records the hosting node returns: a record pointing at the operator's edge and a _tenzro-ownership TXT proof. Verification reads the TXT record before admitting the domain, and TLS is issued automatically on the first request.
tenzro site domain add --hostname app.example.com --site-id <id> \
--owner-did did:tenzro:human:... --did-envelope <hex>
tenzro site domain verify --hostname app.example.com \
--owner-did did:tenzro:human:... --did-envelope <hex>Placement and leases
Deploying does not pin a workload to the node that received it. Nodes announce what they can host: runtime classes, free CPU, memory and disk, TEE support and an hourly TNZO price. Placement filters candidates by runtime class, resources, TEE support and your price ceiling, ranks them by your region hint and then by price, and leases the top nodes for the number of replicas you asked for. If no remote node qualifies, the app serves locally. A reconcile pass re-places any replica whose node goes silent, so the replica count holds without your intervention.
tenzro machine deploy ... --replicas 3 --region-hint <region> --max-price-per-hour <price>
tenzro lease list
tenzro lease get --app-id <app_id>Payment
A site, function or machine can charge per request in TNZO with --price-per-request. The node then gates serving behind an x402 challenge: the caller pays, the node verifies and settles, then serves. Hosting nodes are paid for their leases in TNZO, metered as used.
Methods
| Methods | Access |
|---|---|
tenzro_siteGet, tenzro_listSites, tenzro_functionGet, tenzro_listFunctions, tenzro_machineGet, tenzro_listMachines, tenzro_machineStatus, tenzro_machineSealingKey, tenzro_listLeases, tenzro_getLeasesForApp | open |
tenzro_sitePublish, tenzro_siteRemove, tenzro_siteSetAlias, tenzro_siteRemoveAlias, tenzro_siteSetPlacement, tenzro_siteClaimDomain, tenzro_siteVerifyDomain, tenzro_siteRemoveDomain, tenzro_functionDeploy, tenzro_functionRemove, tenzro_machineDeploy, tenzro_machineRemove | owner (signed DID envelope) |
Host as an operator
Run a node with the cloud role, configure your app domain and edge addresses, and bond for the highest class you offer:
[hosting]
app_domain = "apps.example-operator.tld"
edge_ipv4 = "203.0.113.10"
edge_ipv6 = "2001:db8::10"tenzro stake deposit <amount> --provider-type cloud --cloud-tier machinesServing machines needs a Linux host with KVM. See Operators and roles.