SvelteKit adapter for Bun. Bun.serve(), zero runtime dependencies, copy-paste into your project.
I'm not vibing, I am cooking.
I'm done with the JavaScript dependency circus.
The ecosystem is drowning in packages. Thousands of libraries wrapping trivial logic that any developer (or AI) can read, understand, and maintain in 10 minutes. We npm install without thinking twice, then wonder why node_modules is 500MB for a todo app.
Worse — every dependency is a trust decision. You're letting a stranger's code run in your build, your server, your users' browsers. One maintainer gets compromised, one malicious update slips through, and your project ships malware. This isn't hypothetical. It keeps happening. Supply chain attacks are the easiest way to hit thousands of projects at once, and the JS ecosystem is the #1 target.
So this adapter will never be on npm, yarn, or any registry. Not today, not ever.
Copy the source code into your project. Read it. Own it. That's it. You don't need to trust me, you don't need to trust a registry, you don't need to worry about what happens when I push the next update. The code is right there — you can audit every line.
I do offer a bun add github:... option for convenience, but I genuinely recommend against it. Even for my own code. Don't trust me. Don't trust anyone. Read the source.
If you want to support this project: report issues, star the repo, watch for updates. That's worth more than any download count.
Two ways to install. Pick one.
Full control, no dependency, you own the code. Best for AI-assisted workflows.
src/ directory into your SvelteKit project (e.g. as src/adapter-bun/)svelte.config.js:import adapter from './src/adapter-bun/index.js';
export default {
kit: {
adapter: adapter({
out: 'build',
precompress: true
})
}
};
vite.config.js:import { sveltekit } from '@sveltejs/kit/vite';
import { lifecyclePlugin } from './src/adapter-bun/plugin.js';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit(), lifecyclePlugin()]
});
This is required for sveltekit:startup and sveltekit:shutdown events to work in dev and preview modes. Without it, lifecycle hooks in hooks.server.js won't fire during development.
If you use WebSocket, pass the handler path:
lifecyclePlugin({ websocket: 'src/websocket.js' })
For AI-assisted installation with more detail, use the /install skill in Claude Code.
If you prefer a traditional dependency install:
# latest
bun add github:binsarjr/svelte-adapter-bun
# specific version
bun add github:binsarjr/svelte-adapter-bun#v1.0.0
Then in svelte.config.js:
import adapter from 'svelte-adapter-bun';
export default {
kit: {
adapter: adapter({
out: 'build',
precompress: true
})
}
};
And in vite.config.js:
import { sveltekit } from '@sveltejs/kit/vite';
import { lifecyclePlugin } from 'svelte-adapter-bun/plugin';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit(), lifecyclePlugin()]
});
Note: This pulls directly from the GitHub repo, not npm. Updates require
bun update svelte-adapter-bunor re-installing. You won't be able to edit the adapter source directly — use Option A if you want that.
bunx --bun vite dev
Plain vite dev runs under Node.js — Bun-specific APIs like Bun.serve(), Bun.file(), etc. won't be available. The plugin warns you if Bun runtime is not detected.
bun run build
cd build && bun ./index.js
| Option | Type | Default | Description |
|---|---|---|---|
out |
string |
'build' |
Build output directory |
precompress |
boolean | CompressOptions |
true |
Precompress static assets with gzip/brotli |
envPrefix |
string |
'' |
Prefix for environment variable lookups |
development |
boolean |
false |
Enable development mode |
websocket |
string | false |
false |
Path to WebSocket handler file |
When precompress is an object:
| Option | Type | Default | Description |
|---|---|---|---|
files |
string[] |
(see below) | File extensions to compress |
gzip |
boolean |
true |
Enable gzip compression |
brotli |
boolean |
true |
Enable brotli compression |
Default compressed extensions: html, js, json, css, svg, xml, wasm, txt, ico, mjs, cjs, map
All variables support an optional prefix configured via envPrefix.
| Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
Server listen address |
PORT |
3000 |
Server listen port |
ORIGIN |
(auto) | Override origin URL for request construction |
SOCKET_PATH |
— | Unix socket path (overrides HOST/PORT) |
PROTOCOL_HEADER |
— | Header for protocol detection behind proxy |
HOST_HEADER |
— | Header for host detection behind proxy |
PORT_HEADER |
— | Header for port detection behind proxy |
ADDRESS_HEADER |
— | Header for client IP (e.g. x-forwarded-for) |
XFF_DEPTH |
1 |
Trusted proxy depth for X-Forwarded-For |
BODY_SIZE_LIMIT |
Infinity |
Max request body size (supports K/M/G suffixes) |
SHUTDOWN_TIMEOUT |
30 |
Seconds to wait during graceful shutdown |
IDLE_TIMEOUT |
0 |
Connection idle timeout in seconds |
Create a WebSocket handler file:
// src/websocket.js
/** @param {Request} request @param {import('bun').Server} server */
export function upgrade(request, server) {
const success = server.upgrade(request, {
data: { /* custom data */ }
});
if (success) return undefined;
return new Response('Upgrade failed', { status: 500 });
}
/** @param {import('bun').ServerWebSocket} ws */
export function open(ws) {
console.log('Client connected');
}
/** @param {import('bun').ServerWebSocket} ws @param {string | Buffer} message */
export function message(ws, message) {
ws.send(message);
}
/** @param {import('bun').ServerWebSocket} ws */
export function close(ws) {
console.log('Client disconnected');
}
Point the adapter at it:
adapter({
websocket: 'src/websocket.js'
})
The Bun.Server instance is on event.platform.server in SvelteKit hooks and endpoints — use it for pub/sub, requestIP(), etc.
The handler is exported as a composable function:
import createHandler from './handler.js';
const { httpserver, websocket } = createHandler();
Bun.serve({
fetch: httpserver,
websocket,
port: 8080
});
The server emits process-level events for startup and shutdown. Use them to init and tear down resources like DB connections or caches.
These events work in all modes:
src/files/index.js)src/plugin.js). You must add lifecyclePlugin() to your vite.config.js (see Installation step 3)In dev mode, hooks.server.js loads lazily on the first request. The plugin handles this with sticky event replay — late listeners automatically receive events they missed.
sveltekit:startupFires after Bun.serve() starts (production) or after the Vite HTTP server is listening (dev/preview).
process.on('sveltekit:startup', async ({ server, host, port, socket_path }) => {
await db.connect();
console.log(`DB connected, server listening on ${host}:${port}`);
});
sveltekit:shutdownFires on SIGINT/SIGTERM after server.stop(). A SHUTDOWN_TIMEOUT guard (default 30s) force-exits if listeners hang.
process.on('sveltekit:shutdown', async (reason) => {
await db.disconnect();
await cache.flush();
});
The adapter augments App.Platform:
interface Platform {
req: Request; // Original Bun request
server: import('bun').Server; // Bun server instance
}
Access in hooks or endpoints:
export function handle({ event, resolve }) {
const bunServer = event.platform.server;
return resolve(event);
}
| Feature | adapter-node | adapter-bun |
|---|---|---|
| Runtime | Node.js | Bun |
| Bundler | Rollup | Bun.build() |
| Static server | sirv (npm) | Built-in (zero deps) |
| Compression | node:zlib streams | Bun.gzipSync + node:zlib brotli |
| WebSocket | Not supported | Native Bun WebSocket |
| File serving | fs streams | Bun.file() |
| Client IP | Hardcoded fallback | Bun server.requestIP() |
| Dependencies | rollup, sirv, tiny-glob, etc. | Zero runtime deps |