Deploy & invoke a function
A top-level function is a WASI component you deploy and call by name, with its own version line — independent of any site deployment. Use it when you want a unit of compute that is invoked directly (sync or async), versioned and rolled back on its own, and reused across sites. For the concept, see Functions: the compute primitive; to run one behind a route instead, see Deploy a handler.
A function is owned by a project, just like a site. Every boatramp function …
command respects the global --project flag (env BOATRAMP_PROJECT, falling back
to [publish].project, then the reserved default project), and a function name is
unique only within its project — so acme/resize and beta/resize are distinct
functions.
All of the commands below take --server <url> (or read it from project.cfg) and
require a token with system·admin for writes / invoke, system·read for reads —
or a project role scoped to the function’s project (project_admin:<proj> for
writes / invoke, project_viewer:<proj> for reads).
Scaffold a new function
Start from a template instead of hand-wiring a wasi:http component:
$ boatramp function init greeter
scaffolded greeter in ./greeter
next: cd greeter && boatramp function build
$ cd greeter && boatramp function build
built target/wasm32-wasip2/release/greeter.wasm
deploy: boatramp function deploy <name> --component target/wasm32-wasip2/release/greeter.wasm
function init writes a minimal component (a handle function you edit) plus its
wit/ world; function build compiles it and prints the produced component,
detecting the language from the project files:
--lang rust(default) — awasi:httpcomponent built withcargo build --release --target wasm32-wasip2. Needs thewasm32-wasip2target (rustup target add wasm32-wasip2, or the project’snix developshell).--lang js— a JavaScript component built withjco componentize(fetched version-pinned vianpx, so only Node is required;nix developprovides it).--lang python— a Python component built withcomponentize-py(run version-pinned viauvx, so onlyuvis required;nix developprovides it).
The produced .wasm is a portable WASI component in every case — deploy it with
function deploy, and it runs on the same engine. Note the JS and Python
components bundle their language runtime (~12–18 MB) and so are larger than a Rust
component; pick the language that fits your code.
Run it locally
Before deploying, exercise the component locally — no server, no upload. The harness runs the component in-process through the same engine that serves it in production:
# One request + assertions (exits non-zero if an assertion fails):
$ boatramp function test --component target/wasm32-wasip2/release/greeter.wasm \
--path /hello --expect-status 200 --expect-body "hello"
HTTP 200
hello from your boatramp function (/hello)
ok
# Or serve it on a local port and curl it:
$ boatramp function dev --component target/wasm32-wasip2/release/greeter.wasm --port 8787
serving …/greeter.wasm on http://127.0.0.1:8787 (Ctrl-C to stop)
The harness grants no host capabilities (kv/sql/blobstore/messaging), so it suits
components that only use the HTTP request/response — capability-backed local
testing comes later. function test/dev are in the build compiled with the
handlers feature (the engine).
Deploy a version
Deploy a component .wasm as a named function. The CLI uploads it as a
content-addressed blob first, then registers the version:
$ boatramp function deploy greeter --component ./greeter.wasm
deployed greeter [wasm] a1b2c3d4e5f6
The printed id is the version — the component’s content hash. Deploying the
same bytes again is idempotent; deploying new bytes appends a version and makes it
active. Choose a stronger runtime substrate with
--runtime microvm (or container).
List and inspect
$ boatramp function ls
greeter [wasm] a1b2c3d4e5f6 invoke:greeter
$ boatramp function get greeter
greeter
runtime: wasm
version: a1b2c3d4e5f6
Invoke it
A sync invoke runs the function inline and streams back its response. The
request body is sent to the function; --data / --data-file supply it:
$ boatramp function invoke greeter --data '{"name":"Ada"}'
Hello, Ada!
An async invoke durably enqueues the call and returns an id to poll — the run survives a restart and is retried, then dead-lettered, on failure:
$ boatramp function invoke greeter --async --data '{"name":"Ada"}'
queued 7f3a… [queued]
$ boatramp function invocation greeter 7f3a…
7f3a… [succeeded] attempts=1
result: HTTP 200
Idempotency
Pass --idempotency-key <key> to make an invoke safe to retry: a repeat with the
same key replays the first call’s outcome instead of running the function
again. This holds for both sync and async.
$ boatramp function invoke greeter --idempotency-key order-42 --data '…'
Versions, aliases, and rollback
A top-level function carries its own version line, so you can promote and roll back without touching any site:
# Point a label at a version (e.g. a stable "prod" alias).
$ boatramp function alias greeter prod a1b2c3d4e5f6
# Invoke a specific version or alias instead of the active one.
$ boatramp function invoke greeter --version prod
# Roll the active version back to an earlier one.
$ boatramp function rollback greeter --to a1b2c3d4e5f6
Usage & quotas
Every invocation is metered host-side. Read the aggregate:
$ boatramp function usage greeter
greeter
invocations: 128 (126 ok, 2 failed)
duration: 5310 ms total
bytes: 40960 in / 81920 out
The same counters are exported as Prometheus series
(boatramp_function_invocations_total, …_failures_total,
…_duration_ms_total) — see Observe.
A function may declare a quota in its config, enforced fail-closed (over the
limit ⇒ 429):
max_invocationsover awindow_secswindow — a fixed-window rate limit.max_concurrent— the most in-flight invocations at once (per node).
Scheduled & event triggers
A top-level function can also be reached by a trigger the server dispatches on
its own — no caller. Add one with function trigger add:
# Run the function on a schedule (a durable async invocation each fire).
$ boatramp function trigger add greeter tick --cron "0 * * * *"
# Invoke the function per message on its queue `fn/greeter/jobs`.
$ boatramp function trigger add greeter jobs --queue jobs
# Invoke the function when an object changes under `fn/greeter/uploads/`.
$ boatramp function trigger add greeter onupload --blob uploads/
$ boatramp function trigger ls greeter
jobs [queue]
onupload [blob]
tick [cron]
$ boatramp function trigger rm greeter tick
- A cron fire enqueues a durable invocation (retried, then dead-lettered, like any async invoke).
- A queue trigger claims messages from the function’s own
fn/<name>/<topic>topic and invokes the function once per message, acking on success. - A blob trigger fires when an object changes under the watched prefix — and it
fires for any writer, not just boatramp, because it uses the storage
backend’s native change notification (inotify/FSEvents locally, S3→SQS in the
cloud). The changed key + kind arrive as the invocation’s JSON body. It needs a
watch-capable storage backend: on one that can’t watch, adding the trigger is
refused (a
400, never a silent no-op). In a cluster the scheduler fires each trigger on the leader, exactly once.
Cloud blob triggers (auto-provisioning)
The function trigger add --blob command is identical on every backend — the
environment difference hides behind the storage backend. On the filesystem the
watch is zero-config (inotify/FSEvents). On a cloud object store the native event
pipeline must be created first, so boatramp provisions it for you — “auto-DNS, but
for object-store events.” What boatramp creates is recorded in a managed-notification
ledger and retracted when you remove the trigger, so no cloud resources leak.
Each cloud backend uses its native pipeline:
- S3 (
--blobs s3) — an SQS queue + a queue access policy + a bucketQueueConfiguration(added by read-merge-write, so existing notifications are preserved and an overlapping foreign entry is refused, never clobbered). Fully auto-provisioned. - GCS (
--blobs gcs) — a Pub/Sub topic + subscription + a bucketnotificationConfig. Auto-provisioned except the one-time IAM grant giving the GCS service agentroles/pubsub.publisheron the topic (thedry-runrecipe prints it). - Azure (
--blobs azure) — a Storage Queue (auto-provisioned) fed by an Event Grid subscription. The Event Grid subscription is a one-time management-plane (Azure AD) step thedry-runrecipe prints as anaz eventgridcommand; boatramp manages + consumes the queue.
You pick the behavior with a tier in the server’s boatramp.cfg (the elevated
cloud credentials live server-side, not in the CLI):
serve: (
// dry-run | provision | verify-only | refuse (default)
blob_notify_tier: "provision",
// S3: the AWS account id (scopes the SQS queue policy).
// GCS: the GCP project id (for the topic + notificationConfig).
// Azure: unused (the queue shares the account's shared-key auth).
blob_notify_account_id: "123456789012",
)
dry-run— adding the trigger prints the exact pipeline to apply and does not activate (nothing is mutated, no credentials needed).provision— boatramp creates + reconciles + retracts the pipeline (needs credentials allowed to manage SQS + the bucket notification config).verify-only— you pre-wired the pipeline; boatramp checks it exists, then consumes it.refuse(default) — no pipeline, no provisioning ⇒ the trigger is refused (fail-closed). This is why a cloud blob trigger with no tier configured is a400: the behavior stays conceptually clear, never a silent no-op.
Signed webhooks
To let an external system trigger a function over a public, signature-verified endpoint, deploy it with a webhook secret reference:
$ BOATRAMP_HOOK_SECRET=… boatramp function deploy ingest \
--component ./ingest.wasm --webhook-secret-env BOATRAMP_HOOK_SECRET
Callers then POST /_webhooks/ingest with an X-Boatramp-Signature header holding
the HMAC-SHA256(body, secret) hex (a leading sha256= is accepted). boatramp
verifies the signature constant-time, before the function runs — a missing or
wrong signature is 401, and the secret lives only in the host env var you named,
never in the stored config.
Remove it
$ boatramp function rm greeter
removed greeter
Content-addressed component blobs are shared, so removal leaves them for
prune.