Svelte Smol Adapter
[!WARNING] Deprecated. SvelteKit 3 has an official Bun adapter,
@sveltejs/adapter-bun, which also compiles the app into a single executable. Migrate to it, see Migrating to@sveltejs/adapter-bun. This package only gets SvelteKit 2 fixes until the repository is archived.
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-smolEvery merge to main ships as @orochibraru/svelte-smol@canary; stable
releases go out under latest.
#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#Migrating to @sveltejs/adapter-bun
The official adapter needs SvelteKit 3 and Bun 1.4 or newer. These steps work from svelte-smol on either SvelteKit version.
-
Swap the packages:
bun remove @orochibraru/svelte-smol bun add -d @sveltejs/kit@^3 @sveltejs/adapter-bun -
Configure the adapter in the Vite plugin, and remove
adapterfromsvelte.config.jsif you were on SvelteKit 2:// vite.config.js import adapter from "@sveltejs/adapter-bun"; import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ sveltekit({ adapter: adapter({ buildOptions: { compile: true } }) }), ], });Compiling is opt-in. Without
buildOptions.compile, the build is a Vite-builtbuild/index.jsrun withbun ./build, and the app's productiondependenciesare not bundled into it: ship a productionnode_modulesnext to it. That is also the mode for native (.node) addons, ascompile: falsewas here. -
Build under the Bun runtime:
bun run --bun vite build. -
Rename your options:
svelte-smol, SvelteKit 2 svelte-smol, SvelteKit 3 @sveltejs/adapter-buncompile: true(default)buildOptions: { compile: true }(default)buildOptions: { compile: true }compile: falsebuildOptions: { compile: false }leave buildOptions.compileunsettarget: "bun-linux-x64"buildOptions: { compile: "bun-linux-x64" }same as svelte-smol on SvelteKit 3 name: "app"buildOptions: { compile: { outfile: "app" } }same as svelte-smol on SvelteKit 3 bytecode,minify,sourcemapbuildOptions.*buildOptions.*serveOptionsserverOptionsserverOptions(JSON-serializable only)serveAssets: falseremoved removed, assets are always served n/a buildOptions.splittingremoved healthcheckhealthcheckremoved, see step 6 out,precompressandenvPrefixkeep their meaning.precompressis still ignored when compiling. -
Update your environment:
IDLE_TIMEOUTbecomesCONNECTION_IDLE_TIMEOUT(max255).ASSETS_DIR,HEALTHCHECK_PATHandHEALTHCHECK_TIMEOUTare gone.ORIGINis gone. The public origin comes frompaths.originin the SvelteKit config, fixed at build time, else from the request and the forwarded headers, assuminghttpsunlessPROTOCOL_HEADERsays otherwise. An app served over plain HTTP with neither set fails the CSRF check on every form action: setpaths.origin, or setPROTOCOL_HEADER=x-forwarded-protobehind a proxy that sends it.- With
envPrefix, the server refuses to start when it sees a prefixed variable it doesn't know. Delete leftovers likeMY_APP_ORIGINorMY_APP_HEALTHCHECK_TIMEOUT.
-
Replace the health check. The endpoint is a regular route:
// src/routes/_health/+server.ts export const prerender = false; export const GET = () => Response.json( { status: "ok" }, { headers: { "cache-control": "no-store" } }, );
The probe is a script you compile yourself, so it still runs in an image
without curl:
// healthcheck.ts
const port = process.env.PORT ?? "3000";
try {
const response = await fetch(`http://127.0.0.1:${port}/_health`, {
signal: AbortSignal.timeout(2000),
});
process.exit(response.ok ? 0 : 1);
} catch {
process.exit(1);
}RUN bun build --compile --minify ./healthcheck.ts --outfile build/healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD ["./build/healthcheck"]Pass the same --target as buildOptions.compile when cross-compiling.
One behaviour has no replacement: a compiled executable answers a Range
request on an embedded asset with 200 and the whole file, so a <video>
downloads everything before it can seek. Serve large media from a CDN or the
proxy, or don't compile.
#Output
build/
├── server # the compiled executable
├── client/ # static assets, served by the executable
└── prerendered/ # prerendered pages, served by the executableDeploy 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.jsPure-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
| Variable | Default | Purpose |
|---|---|---|
HOST | 0.0.0.0 | Listen address |
PORT | 3000 | Listen port |
SOCKET_PATH | — | Listen on a Unix socket instead of HOST/PORT |
ASSETS_DIR | — | Override where client/ and prerendered/ are looked up (absolute, or relative to the binary) |
ORIGIN | — | Public origin, read at runtime (Kit 2 and 3; on Kit 3 it overrides paths.origin) |
PROTOCOL_HEADER | — | Header carrying the forwarded protocol (e.g. x-forwarded-proto) |
HOST_HEADER | — | Header carrying the forwarded host |
PORT_HEADER | — | Header carrying the forwarded port |
ADDRESS_HEADER | — | Header carrying the client address (e.g. x-forwarded-for) |
XFF_DEPTH | 1 | Trusted-proxy depth when ADDRESS_HEADER=x-forwarded-for |
BODY_SIZE_LIMIT | 512K | Max request body size (K/M/G suffixes allowed) |
IDLE_TIMEOUT | 10 | Bun socket idle timeout in seconds (SSE responses opt out) |
SHUTDOWN_TIMEOUT | 30 | Seconds to wait for in-flight requests on SIGINT/SIGTERM |
HEALTHCHECK_PATH | /_health | Endpoint the healthcheck binary probes (must match the healthcheck option) |
HEALTHCHECK_TIMEOUT | 2000 | healthcheck 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;
--compileembeds 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 releaser from Conventional Commits:
fix:perf:revert:feat:docs:refactor:, and breaking changes (feat!:or aBREAKING CHANGE:footer) → patchtest:chore:build:ci:style:→ no release- Pushes that don't touch the published package (
index.ts,kit2/,kit3/,package.json,tsconfig*.json) don't run a release at all - Every release is published from
mainto npmlatest, for both SvelteKit versions
This guide lives in the project repo: edit it there, and this page follows within a day.