fn0 Platform Overview
fn0 (pronounced "f-n-zero") is a FaaS (Function-as-a-Service) platform powered by Wasmtime. It executes WebAssembly components compiled to WASI 0.3.0 (Component Model).
Core Concepts
Execution Model
fn0 uses the same model as Cloudflare Workers:
- A single WASM instance (and, for JS deployments, a single V8 isolate) is reused to serve many concurrent requests on the same worker thread.
- Handlers must be effectively stateless across requests. Module-level mutable state must not carry information between requests because another request may be interleaved at any
awaitpoint. - Module-level initialization runs once. Per-request setup belongs inside the handler.
fn0 does not enforce this contract. Violating it causes request-level data leakage or inconsistent behavior.
Response Specification
The WASM component returns an HTTP response. fn0 inspects it:
- If the response has status 200 and the header
x-fn0-next: js, fn0 delegates to the named runtime (currently onlyjs— the Ski JavaScript runtime). - Otherwise fn0 forwards the response as-is.
- All
x-fn0-*headers are stripped from the response before it is sent to the client.
Forte uses x-fn0-next: js for page handlers (SSR) and skips it for API endpoints and actions.
fn0 Cloud Limits
These limits apply to fn0 Cloud. Self-hosted deployments can remove them.
| Limit | Value |
|---|---|
| Request headers | 128 KB |
| Request body | 100 MB |
| Response headers | 128 KB |
| Response body | Unlimited |
| Memory | 128 MB |
| CPU time | 50 ms |
| Max duration | 15 seconds |
| Subrequests (external HTTP) | 50 per request |
The request-body value is a transport limit. Applications can process bodies up to that limit with bounded memory only by streaming; buffering remains subject to the 128 MB memory limit. Durable file uploads should normally use presigned object-storage URLs, while streaming through compute remains supported for cases that require it. Request and response streaming is implemented end to end; see HTTP Body Streaming.
Cluster Architecture (Internal)
- Monolith architecture — no microservices.
- Public ingress (OCI): Cloudflare (orange proxy) → OCI L4 Network Load Balancer (always-free) → worker pool. Workers run in an OCI InstancePool with AutoScaling; the NLB is the single entry.
- Intra-node dispatch: Each worker node runs a pool of N OS threads (default = CPU count). Incoming requests are dispatched to a specific thread using a hash of the
project_idmodulo N, so a given project always lands on the same thread within a node. Thread queue capacity is 256 per thread; a full queue returns HTTP 503. - Blue-green deploys:
fn0-worker-agentpolls the control DB for the target worker image, starts the new container, waits for it to pass health checks, and then drains and stops the old one.fn0-worker-proxyreroutes new TCP connections to the active container. Established WebSockets receive1012and the old container remains alive through their graceful close timeout. - WebSocket routing: The owning
fn0-workerkeeps the socket. The fn0-control Turso database stores connection ownership, and direct TLS-protected QUIC streams carry remotesendanddisconnectcommands. Turso never carries message payloads; bounded control reconciliation removes confirmed stale records without leases or heartbeats. - WASM modules are cached in memory after the first download. On subsequent requests, the compiled bundle version is checked against the manifest; re-download only if the version changed.
Packages
| Package | Version | Description |
|---|---|---|
fn0 | 0.7.1 | Core FaaS runtime (ExecutionContext, Bundle, build_engine) |
fn0-cli | 0.1.23 | Local development CLI |
fn0-worker | 0.4.18 | Worker binary (distributed execution node) |
fn0-worker-agent | 0.1.9 | Per-instance container supervisor (blue-green deploys, in-host TCP proxy) |
fn0-worker-proxy | 0.1.1 | Tiny TCP forwarder fronting fn0-worker containers; polls a target file written by worker-agent |
fn0-deploy | 0.3.5 | fn0 Cloud deployment client |
fn0-wasmtime | 0.2.1 | Wasmtime wrapper with fn0-specific config |
fn0-ski | 0.1.13 | WinterCG-compatible JS runtime (Deno-based, no Node.js) |
fn0-compiler | 0.1.0 | CLI tool: compiles .wasm → .cwasm (Wasmtime pre-compiled native format); used internally by the platform |
fn0-cli Commands
The fn0 CLI (fn0/cli) provides tooling for projects deployed directly to fn0 without Forte:
| Command | Description |
|---|---|
fn0 init [--name <name>] | Scaffold a new fn0 project (prompts for framework and language) |
fn0 build | Compile the project to WASM |
fn0 local [--port <port>] | Run locally on the given port (default: auto) |
fn0 deploy | Build and deploy to fn0 Cloud |
fn0 destroy | Delete the deployed project |
fn0 rename <new-name> | Rename the deployed project |
fn0 login [token] | Authenticate with fn0 Cloud; interactive login uses a one-time browser code |
fn0 admin run <task> | Run an admin task against the deployed app |
fn0 env set <key> <value> [--secret] | Set an env entry (plain by default; --secret encrypts via vault and injects through vault_hijack at runtime) |
fn0 env list | List env entries with their kind (plain / secret) |
fn0 env unset <key> | Remove an env entry |
fn0 purge <key>... | Invalidate the CDN edge copy of one or more public objects (needed after presigned PUT uploads; public::put already purges automatically) |
fn0 purge-page <path>... | Invalidate the CDN edge copy of one or more cache_static pages (use when runtime data changes; see pages.md) |
Note: Most Forte developers use
forteCLI instead offn0CLI.fn0CLI is for projects that use fn0 as a raw FaaS platform (e.g., Hono-based TypeScript apps).
Supported Languages
- Rust — primary target; compiles to
wasm32-wasip2 - JavaScript / TypeScript — via the Ski runtime (WinterCG subset, no Node.js APIs)
Supported Cloud Providers
- Oracle Cloud Infrastructure (OCI) — instance pool, NLB, vault
Supported CDN Providers
- Cloudflare Workers (integration)
Supported Code Storage Providers
- File system (including NFS, e.g. AWS EFS)
- S3 and compatible object storage (via
opendal)
Observability
fn0 has built-in OpenTelemetry support:
- OTLP span exporter via
fn0/fn0/src/otlp_hijack.rs(worker-side) andforte/sdk/src/otel.rs(WASM-side) - Structured logging via
tracing - Distributed tracing with per-request spans
For Forte apps running on fn0 Cloud, traces are exported to http://fn0-otel.fn0.dev/v1/traces. The service name is controlled via the OTEL_SERVICE_NAME environment variable (defaults to "forte-app").
For self-hosted fn0 worker deployments, configure the OTLP collector via environment variables on the worker binary:
| Variable | Required | Default | Description |
|---|---|---|---|
OTLP_ENDPOINT | Yes | — | Base URL of an OTLP/HTTP protobuf collector, such as the worker-local collecty (http://127.0.0.1:4318) |
FN0_PLATFORM_TELEMETRY_TENANT_ID | Yes | — | Signy tenant for the worker's own telemetry that belongs to no single project |
FN0_OTLP_PLACEHOLDER_HOST | No | fn0-otel.fn0.dev | Placeholder hostname used inside WASM apps |
The worker intercepts outgoing OTLP requests that target FN0_OTLP_PLACEHOLDER_HOST. It accepts only uncompressed application/x-protobuf exports on /v1/traces, /v1/metrics, and /v1/logs, sets tenant.id on every resource to the calling project's id, applies the metric cardinality caps, and forwards the result to OTLP_ENDPOINT.
Each project is its own Signy tenant. The worker's request spans, the request, static page, and CPU metrics it records for a project, and the lines it captures from that project's stdout and stderr all go to the project's tenant. The worker's own logs, its failure counters and its capacity metrics go to the platform tenant named by FN0_PLATFORM_TELEMETRY_TENANT_ID. Signy drops data for a tenant that has no pushed policy, so control registers each project with Signy, for 30 days of logs, traces and metrics alike.
Trace sampling is decided once, by the worker, when a request arrives: 1% of requests are traced, and background loops such as manifest polling are never traced. The worker passes its decision to the guest in a traceparent header, and an incoming traceparent from a visitor is discarded.
What the worker writes as a log is the failure and the state change, not the ordinary request: a guest that answered with a server error, a request that ran out of time (once, from the side that answers the client), a static page that could not be generated, and the first uncacheable answer of each deployed version. A slow request that succeeded is a duration on fn0.http.server.request.duration and a span in whichever trace was sampled; it is not a log line. A guest's captured output keeps the level it was written at, with the stream it came from as an attribute, because a library that writes its whole output to stderr is not a library reporting errors.
Worker Environment Variables (Self-Hosting)
When running fn0-worker yourself, set these environment variables on the binary:
Storage
| Variable | Required | Default | Description |
|---|---|---|---|
CWASM_BUCKET | Yes | — | S3 bucket name for pre-compiled .cwasm bundles |
S3_ENDPOINT | Yes | — | S3-compatible endpoint URL |
S3_REGION | No | us-east-1 | S3 region |
AWS_ACCESS_KEY_ID | Yes | — | S3 access key |
AWS_SECRET_ACCESS_KEY | Yes | — | S3 secret key |
FN0_BUNDLE_CACHE_SIZE_BYTES | No | 536870912 (512 MB) | In-memory bundle cache size |
Networking / TLS
| Variable | Required | Default | Description |
|---|---|---|---|
HTTP_PORT | No | 443 | Port for user HTTPS traffic |
FN0_WORKER_OPS_PORT | No | 9090 | Port for ops endpoints (/ready, /drain, /status, /health) |
ORIGIN_CERT_PEM | Yes | — | PEM-encoded TLS origin certificate (or use ORIGIN_CERT_PEM_BASE64) |
ORIGIN_KEY_PEM | Yes | — | PEM-encoded TLS private key (or use ORIGIN_KEY_PEM_BASE64) |
FN0_APEX_DOMAIN | No | — | Apex domain to route when no project hostname matches |
FN0_APEX_PROJECT_ID | No | — | Project ID to serve for FN0_APEX_DOMAIN requests; both must be set together |
FN0_ALLOW_PRIVATE_OUTBOUND_DESTINATIONS | No | false | true lets application HTTP, fetch, and WebSockets reach loopback and private addresses; see Limits |
The first byte on HTTP_PORT: 0x16 (TLS ClientHello) routes to user traffic; anything else routes to health checks. FN0_WORKER_OPS_PORT serves ops endpoints unconditionally.
Telemetry
| Variable | Required | Default | Description |
|---|---|---|---|
OTLP_ENDPOINT | Yes | — | OTLP/HTTP protobuf collector for the worker's own telemetry and forwarded guest exports |
FN0_PLATFORM_TELEMETRY_TENANT_ID | Yes | — | Signy tenant for the worker's own telemetry that belongs to no single project |
FN0_OTLP_PLACEHOLDER_HOST | No | fn0-otel.fn0.dev | Placeholder hostname intercepted inside WASM apps |
Platform Services
| Variable | Required | Default | Description |
|---|---|---|---|
TURSO_GROUP_TOKEN | Yes | — | Auth token for the Turso group (used by turso_hijack) |
TURSO_DB_HOST_SUFFIX | Yes | — | Host suffix for Turso database URLs (e.g. .turso.io) |
TURSO_PLACEHOLDER_HOST | No | fn0-db.fn0.dev | Placeholder hostname intercepted for Turso requests |
FN0_CONTROL_PROJECT_ID | Yes | — | Project ID of the fn0 control service; used by public storage and static page cache hijacks |
FN0_OBJECT_STORAGE_PLACEHOLDER_HOST | No | fn0-object-storage.fn0.dev | Placeholder hostname for private object storage requests |
FN0_PUBLIC_STORAGE_PLACEHOLDER_HOST | No | fn0-public-storage.fn0.dev | Placeholder hostname for public object storage requests |
FN0_STATIC_PAGE_CACHE_PLACEHOLDER_HOST | No | fn0-static-page-cache.fn0.dev | Placeholder hostname for static page cache purge requests |
OCI Vault
| Variable | Required | Description |
|---|---|---|
FN0_VAULT_CRYPTO_ENDPOINT | Yes | OCI Vault cryptographic endpoint URL |
FN0_VAULT_KEY_OCID | Yes | OCID of the encryption key |
FN0_VAULT_OCI_TENANCY_ID | Yes | OCI tenancy OCID |
FN0_VAULT_OCI_USER_ID | Yes | OCI user OCID |
FN0_VAULT_OCI_FINGERPRINT | Yes | API key fingerprint |
FN0_VAULT_OCI_PRIVATE_KEY_BASE64 | Yes | Base64-encoded PEM private key for the OCI API signing key |
Hijack Architecture
fn0 uses "hijack" components to inject platform services into the WASM execution environment without modifying the user's code:
| Hijack | Purpose |
|---|---|
doc_db_hijack | Routes semantic document database requests to the host document service |
turso_hijack | Injects the legacy/direct Turso/libSQL connection |
object_storage_hijack | Routes & SigV4-signs per-project private object storage requests |
public_storage_hijack | Routes public object storage requests (no SigV4 signing required) |
otlp_hijack | Injects OpenTelemetry OTLP endpoint |
queue_hijack | Intercepts outgoing queue requests |
vault_hijack | Injects secrets (Vault integration) |
websocket_hijack | Dispatches WebSocket send, named singleton send, and disconnect commands via QUIC |
static_page_cache_hijack | Routes static_page_cache::purge calls; shares the hourly purge budget with public storage purges |
cross_project_enqueue_hijack | Routes cross-project queue enqueue calls |
cross_project_invoke_hijack | Routes cross-project direct invocations |
These are configured on ExecutionContext via builder methods:
let ctx = ExecutionContext::new(engine, linker, bundle_cache)
.with_doc_db_hijack(doc_db_hijack)
.with_turso_hijack(turso_hijack)
.with_otlp_hijack(otlp_hijack)
.with_queue_hijack(queue_hijack)
.with_cross_project_enqueue_hijack(enqueue_hijack)
.with_cross_project_invoke_hijack(invoke_hijack)
.with_vault_hijack(vault_hijack)
.with_object_storage_hijack(object_storage_hijack)
.with_public_storage_hijack(public_storage_hijack)
.with_websocket_hijack(websocket_hijack)
.with_static_page_cache_hijack(static_page_cache_hijack)
.with_guest_outbound_http(guest_outbound_http);
with_guest_outbound_http routes the application's own HTTP requests and JavaScript fetch
through the outbound destination policy and the project egress budget. Without it, requests use
wasmtime's default sender with neither check, which is what forte dev does.