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-17 (731f1b2), an ahead-of-time JavaScript compiler that is itself early. Node APIs (nodejs_compat) are the gap you are most likely to hit.
What ships in the file
Your code and the pieces it needs to run, compiled into one executable.
State lives beside it
On first run, the executable creates <name>.data/ for persistent state: one SQLite file for the shared binding tables, a separate file for each D1 database, and files for R2 object data. It uses the same directory layout as sproutboat dev.
- Native handler
- Porffor compiles your JavaScript handler to machine code.
- Local bindings
- KV, D1, R2, queues, Durable Objects and analytics use local storage backed by SQLite.
- Embedded SQLite
- The database engine is compiled into the executable.
- Verified HTTPS
- BearSSL and Mozilla root certificates are included for outbound fetch() requests.
- Static assets
- Your static files are embedded at build time, up to 8 MB in total.
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 |
| Streams6 | ReadableStream, WritableStream, TransformStream, and response bodies streamed as they are writtenServer-Sent Events and long responses work; a client that disconnects cancels the stream. | - |
| HTMLRewriter7 | Rewrite HTML responses with element, text, comment and document handlersOutput matches lol-html, the engine workerd uses. | - |
| Compression streams | CompressionStream and DecompressionStream: gzip, deflate, deflate-rawOutput decompresses with zlib, and zlib's output decompresses here. | - |
- 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.
- Request bodies are still read whole (up to 1 MiB).
- Rewrites the whole body at once rather than streaming it; no :nth-child-style pseudo-classes.
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.