One file. No runtime.

sproutboat build --standalone compiles your handler into a native Linux executable. Copy it to a Linux host, set PORT, and it serves. No Node, Bun or container on the target.

$ sproutboat build --standalone

Alpha. Built on Porffor alpha-10 (08ac7ee), an ahead-of-time JavaScript compiler that is itself early. Streams are the gap you are most likely to hit.

Inside the binary

  • Your handlercompiled to machine code by Porffor
  • The bindingsKV, D1, R2, queues, Durable Objects, analytics: SQL against a local file
  • SQLitethe amalgamation, compiled in
  • BearSSL and Mozilla rootsso fetch() can speak https
  • Your static assetsbaked into the binary, up to 8 MB

State lands in <name>.data/: one SQLite file for the platform tables and one per D1 database, the same layout sproutboat dev writes.

Supported bindings

Every binding a deployed sprout has, except service bindings. Each example is a runnable project.

BindingWhat it isExample
Handlers export default { fetch }: one request in, one Response out hello
KV key-value store: get / put / delete / list kv
D1 SQL database: prepared statements, batch, transactions d1
R21 S3-style object storage with sha256 etags r2
Queues a producer handle plus a queue(batch) consumer, with retries queue
Durable Objects2 addressable single-threaded instances, each with its own storageIncludes alarms: setAlarm / getAlarm / deleteAlarm and the alarm() handler. durable-object
Cron triggers scheduled(event), fired on cron expressionsA standalone binary runs its own timers and refuses triggers arriving over the network. cron
Static assets3 files served from a directory via env.ASSETS.fetch() assets
Analytics Engine writeDataPoint() and query() analytics
Rate limiting4 env.NAME.limit({ key }) -> { success }, a fixed window per key ratelimit
Vars & secrets config values, and secrets that never enter the binaryA standalone binary reads secrets from the environment and throws the first time a handler touches one that's missing. vars-secrets
Outbound fetch fetch() to an allowlisted host, http and httpsTLS in a standalone binary is BearSSL with the Mozilla root set compiled in. outbound-fetch
Service bindingsNot supported env.PEER.fetch() to another deployment on the same nodeA service call routes through the node's edge, which a single binary does not have. -
ctx.waitUntil5 work that outlives the response, on fetch/scheduled/queue waituntil
Streams6Not supported ReadableStream / WritableStream, and streaming response bodies -
  1. An object is held whole in memory both ways. Keep them small for now; see the note in the docs.
  2. An alarm is not yet guaranteed to avoid running alongside a request to the same object.
  3. Compiled into a standalone binary, capped at 8 MB.
  4. One node uses a fixed window; requests near a window boundary can exceed the intended per-interval rate.
  5. In-process drain, capped at 25s.
  6. Blocked upstream in Porffor (issue #349); a response body is a whole string today.

What differs from a deployed sprout

Each one follows from there being nothing else on the machine.

Inbound TLS is not its job

It serves plain HTTP on $PORT. Put Caddy, nginx or a tunnel in front to terminate TLS.

Outbound TLS is

fetch("https://…") verifies against the Mozilla root set through BearSSL, compiled in.

Triggers stay internal

Cron ticks, queue batches and alarms run on timers inside the process. Triggers arriving over the network are refused.

No service bindings

A worker-to-worker call routes through a node's edge, and a standalone binary has no node.