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.
| Binding | What it is | Example |
|---|---|---|
| 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 | - |
- An object is held whole in memory both ways. Keep them small for now; see the note in the docs.
- An alarm is not yet guaranteed to avoid running alongside a request to the same object.
- Compiled into a standalone binary, capped at 8 MB.
- One node uses a fixed window; requests near a window boundary can exceed the intended per-interval rate.
- In-process drain, capped at 25s.
- 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.