Menu
svelte-smol docs

Svelte Smol Adapter

A SvelteKit adapter that compiles your app into a single standalone executable with bun build --compile. No node_modules, no JS files to ship, just one binary plus its static assets.

Install

bun add -d @orochibraru/svelte-smol

Usage

// svelte.config.js
import adapter from "@orochibraru/svelte-smol";

export default {
  kit: {
    adapter: adapter(),
  },
};

The compile step runs under the Bun runtime, so build with:

bun run vite build

Output

build/
├── server        # the compiled executable
├── client/       # static assets, served by the executable
└── prerendered/  # prerendered pages, served by the executable

Deploy the whole build/ directory (or just server if a proxy/CDN serves the assets, see serveAssets). The executable locates client/ and prerendered/ relative to its own path, so it can be run from any working directory:

./build/server

compile: false

bun build --compile bundles every dependency into the binary, and a native (.node) addon like sharp or better-sqlite3 can't be bundled that way. Set compile: false to emit a plain bundle instead:

build/
├── index.js       # the server bundle, run with `bun`
├── healthcheck    # still a compiled binary
├── client/
└── prerendered/
bun run ./build/index.js

Pure-JS dependencies are still bundled into index.js. A native addon can't be, so its require stays in the output and resolves from node_modules at runtime (looked up from index.js's own location, so the working directory doesn't matter). Ship node_modules for those — a production install is enough, since everything that got bundled needn't be there.

Everything else — env vars, the healthcheck binary, serveAssets, instrumentation — works the same.

Options

adapter({
  out: "build", // output directory
  name: "server", // executable filename within `out`
  compile: true, // false → emit build/index.js (run with `bun`) instead of a binary
  target: undefined, // cross-compile target, e.g. "bun-linux-x64"
  bytecode: false, // embed a V8 bytecode cache (faster cold start, bigger binary)
  minify: false, // minify the bundled server code
  sourcemap: false, // embed a source map for server stack traces
  precompress: false, // emit + serve .gz / .br sibling files
  healthcheck: true, // also compile `build/healthcheck` + expose GET /_health
  envPrefix: "", // prefix for the runtime env vars below
  serveAssets: true, // serve client/ and prerendered/ from the binary
  serveOptions: {}, // extra Bun.serve() options (tls, reusePort, …)
});

Cross-compilation

target accepts any Bun compile target, e.g. "bun-linux-x64", "bun-linux-arm64-musl" (Alpine), "bun-darwin-arm64", "bun-windows-x64", with optional -modern / -baseline SIMD suffixes. Bun downloads the matching runtime the first time you use a target.

Runtime environment variables

VariableDefaultPurpose
HOST0.0.0.0Listen address
PORT3000Listen port
SOCKET_PATHListen on a Unix socket instead of HOST/PORT
ASSETS_DIROverride where client/ and prerendered/ are looked up (absolute, or relative to the binary)
ORIGINAbsolute origin used for request URL resolution
PROTOCOL_HEADERHeader carrying the forwarded protocol (e.g. x-forwarded-proto)
HOST_HEADERHeader carrying the forwarded host
PORT_HEADERHeader carrying the forwarded port
ADDRESS_HEADERHeader carrying the client address (e.g. x-forwarded-for)
XFF_DEPTH1Trusted-proxy depth when ADDRESS_HEADER=x-forwarded-for
BODY_SIZE_LIMIT512KMax request body size (K/M/G suffixes allowed)
IDLE_TIMEOUT10Bun socket idle timeout in seconds (SSE responses opt out)
SHUTDOWN_TIMEOUT30Seconds to wait for in-flight requests on SIGINT/SIGTERM
HEALTHCHECK_PATH/_healthEndpoint the healthcheck binary probes (must match the healthcheck option)
HEALTHCHECK_TIMEOUT2000healthcheck binary request timeout in ms

Set envPrefix to namespace these (envPrefix: "MY_APP_"MY_APP_PORT).

Health check

With healthcheck enabled (the default) the build also produces build/healthcheck — a tiny executable that requests GET /_health over loopback (or the Unix socket) and exits 0 when the server answers 200, 1 otherwise. GET /_health returns { "status": "ok", uptime, rss, pid, timestamp }. Drop it straight into Docker:

HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
    CMD ["./build/healthcheck"]

It reads the same HOST / PORT / SOCKET_PATH as the server, so no extra wiring is needed.

Notes

  • The SvelteKit server code is JavaScript emitted by Vite; --compile embeds it in the binary, so nothing but the executable ships. Native (.node) modules in your dependencies are the one thing that can't be bundled this way.
  • WebSockets, read() from $app/server, prerendering, and server instrumentation are all supported.

Releases

Automated by semantic-release from Conventional Commits:

  • fix: / perf:patch, feat:minor, feat!: or a BREAKING CHANGE: footer → major
  • docs: refactor: test: chore: build: ci: style:no release
  • feat: / fix: scoped to ci, build, deps, dev, repo, test, example, releaseno release (they don't touch the published package)

This guide lives in the project repo: edit it there, and this page follows within a day.