Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Functions: the compute primitive

Everything boatramp runs is a function: a portable WASI 0.2 component plus the capabilities it is granted. A function is the one artifact the engine executes. What differs between “a handler”, “a consumer”, “a cron”, and “an invoked function” is not the code — it is the trigger that reaches it.

This is the mental model to carry through the rest of the docs:

One primitive, two views. A function is the compute noun. A handler is a function reached by an HTTP route; a consumer is one reached by a queue topic; a cron is one reached by a timer; an invoked function is one reached by name. Same component, same sandbox, same bindings — different door.

You have almost certainly already written a function: a handler is one, viewed through a route. Nothing about that changes. The function framing just names the thing the route triggers, so the same component can also be invoked directly, put on a schedule, or wired into a workflow — without being rewritten.

Why a component, not a container image

A boatramp function is a standards-based WASI component, and that is the whole point of the portability claim. The same .wasm runs unmodified on boatramp, on another WASI 0.2 host (wasmtime, Spin, workerd), and — because the contract is the component model, not a boatramp API — it is not locked to us. Instantiation is sub-millisecond, the memory footprint is small, and the sandbox is strong: the guest can only touch the host capabilities you grant (wasi:keyvalue, sql, wasi:blobstore, wasi:messaging, and invoke — calling another function in-process). Reach for a function first.

Triggers: the many doors to one function

A trigger is a separate thing from the function it fires, and many triggers can point at the same function version. That is what lets one component be both a route and a cron:

TriggerThe familiar nameWhat fires it
Routehandleran HTTP request matching a host + path
Queueconsumera message on a topic
Timercrona schedule
Invoke(the FaaS verb)a call by function name
Webhooka signature-verified inbound POST
Streamstreamhost-native SSE / WebSocket fan-out (no component)

A site’s handlers, consumers, crons, and streams in project.cfg are functions with triggers — they desugar to exactly that, with no behavioural change. You keep authoring them the familiar way; the engine runs one path.

Site-scoped vs. top-level functions

A function has an owner, and the owner sets how it is addressed and versioned:

  • A site-scoped function is part of a site’s deployment. It versions and rolls back atomically with the deploy (deploy-pinned), and it is the shape you get from a handlers / consumers entry. This is the default and needs no new concept — it is your handler.
  • A top-level function is owned by a project/tenant, not a single deploy. It carries its own version line — deploy a new component version, alias a label like prod at a version, rollback independently — and it is invoked by name. This is the FaaS surface: see Deploy & invoke a function.

Calling another function in-process

A function reaches a sibling by name, without leaving the sandbox for a network round-trip, through the invoke capability. It is the same HTTP-shaped call the platform uses to invoke a function from the outside — method, path, headers, body in; status, headers, body back — but it dispatches on the same node, in-process, so there is no re-authentication, no extra hop, and the call is metered and rate-limited against the callee’s own quota exactly as an external invoke is.

Grant it like any other capability: the function must import invoke, and — because letting a compromised function reach any internal function would be a real blast radius — the operator names an allowlist of callable targets. Each entry may use * wildcards, so one mechanism spans the whole range:

// project.cfg — a function that may call one family and one specific sibling
(
  imports: ["invoke"],
  invoke_targets: ["img-*", "audit-log"],  // deny by default: empty ⇒ can call nothing
)

["*"] lets it call any sibling; ["resize"] exactly one. A call to a name outside the list is refused (target-not-allowed) before the callee runs. The host also caps the function-to-function call depth, so a cycle (A→B→A) is stopped with loop-detected rather than nesting until the node is exhausted — the one guard that makes reentrant invocation safe.

The same grant is available to a site handler (a routing.handlers route in project.cfg), so a request handler reached over HTTP with the end user’s bearer can also fan out to sibling functions — the mesh-orchestrator shape. Give the handler imports: ["invoke"] and its own invoke_targets, gated by the site’s allow_imports, exactly as for a function:

// project.cfg — a route handler that authenticates the user, then calls workers
routing: (
  handlers: [
    ( route: "/agent/**", component: "orchestrator.wasm", methods: ["POST"],
      imports: ["invoke"],
      invoke_targets: ["tool-*"] ),  // deny by default: empty ⇒ can call nothing
  ],
),

A handler is the root of a call chain (depth 0), and — unlike the platform’s external function-invoke path, which stamps the control-plane token — an in-process invoke passes the caller’s request headers through verbatim, so the user’s Authorization reaches the callee unchanged (no forwarding to wire up by hand; see handler bindings).

Reach for invoke to compose functions directly (a thin API function fanning out to workers); reach for a workflow when you want declarative orchestration with retries, compensation, and durable state.

The runtime is a knob, not a different thing

The three isolation substrates — an in-process Wasm sandbox, a shared-kernel container, or a hardware-isolated microVM — are a per-function runtime choice, not three different kinds of compute. wasm is the default and scales to zero by instantiation; a function that needs to run an arbitrary Linux program, or stronger isolation for untrusted code, selects microvm or container. The trigger, the versioning, and the addressing are the same whichever substrate runs it.

Where to go next