Skip to content

Runtime

Slingshot runs on Bun, Node.js, worker-style edge hosts, and AWS Lambda. Swap the runtime package or host wrapper and the application logic stays the same.

The SlingshotRuntime interface abstracts the runtime-specific capabilities Slingshot depends on:

CapabilityBunNode.jsEdge / Workers
Password hashingBun.password (argon2id built-in)argon2 packageWeb Crypto PBKDF2 by default
SQLitebun:sqlite (built-in)better-sqlite3 packageNot supported
HTTP serverBun.serve@hono/node-server + Node’s http.ServerExport a fetch handler
File I/OBun.file, Bun.writenode:fs/promisesRead via fileStore; no filesystem writes
GlobBun.Globfast-glob packageNot supported

Everything else - routing, plugins, database adapters, event bus, auth, actor identity - is runtime-agnostic.

Terminal window
bun add @lastshotlabs/slingshot-runtime-bun

No extra dependencies. Bun includes SQLite, password hashing, HTTP serving, and file I/O built-in. The Bun runtime is auto-detected, so you typically don’t need to pass it.

app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
export default defineApp({
port: 3000,
// runtime omitted — Bun is the default
});
Terminal window
bun add @lastshotlabs/slingshot-runtime-node
bun add better-sqlite3 argon2 @hono/node-server ws fast-glob
app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
import { nodeRuntime } from '@lastshotlabs/slingshot-runtime-node';
export default defineApp({
runtime: nodeRuntime(),
port: 3000,
// ...
});
Terminal window
bun add @lastshotlabs/slingshot-runtime-edge

Use @lastshotlabs/slingshot-runtime-edge when Slingshot is hosted inside a worker platform such as Cloudflare Workers or Deno Deploy.

import { edgeRuntime } from '@lastshotlabs/slingshot-runtime-edge';
const runtime = edgeRuntime();

The edge runtime intentionally omits several host features:

  • no SQLite
  • no filesystem writes
  • no glob scanning
  • no server.listen()
  • supportsAsyncLocalStorage: false

If you need bundled file reads, pass fileStore. If you need ISR in a multi-instance worker deployment, pair slingshot-ssr with createKvIsrCache() from @lastshotlabs/slingshot-runtime-edge/kv.

Use Bun when:

  • You want the fastest startup and best SQLite performance
  • You’re deploying to an environment where Bun is available
  • You want zero extra dependencies for SQLite and password hashing
  • You’re using WebSockets

Use Node.js when:

  • Your deployment environment doesn’t support Bun
  • You need to run in a Node-only CI or serverless environment
  • You’re using native npm modules compiled for Node.js

Use the edge runtime when:

  • You’re deploying to Cloudflare Workers, Deno Deploy, or another fetch-handler host
  • You can live without SQLite and local filesystem writes
  • You want SSR or route handling in a worker-friendly runtime envelope

slingshot start discovers app.config.ts and uses the runtime declared in defineApp(...). Pass a non-Bun runtime explicitly when needed:

app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
import { nodeRuntime } from '@lastshotlabs/slingshot-runtime-node';
export default defineApp({
runtime: nodeRuntime(),
port: 3000,
});

Worker-host deployments usually construct the app in the worker entry module rather than calling slingshot start.

Most features work identically across runtimes. The main differences are:

FeatureBunNode.jsEdge / Workers
WebSocketsNative (Bun.serve pub/sub)Via ws packageHost-specific, not provided by runtime-edge
SQLite WAL modeAutomaticExplicit PRAGMA journal_mode=WALNot available
Process signalsBun.signalprocess.on('SIGTERM', ...)Not available
Hot reload--hot flagRequires nodemon or similarHost-platform specific
Draft mode / ALS-dependent helpersFull supportFull supportDegraded when ALS is required

At bootstrap time, runtime selection does not change the plugin contract: if you pass root ws config to createApp() or createServer(), Slingshot threads that draft through infrastructure, copies ws.endpoints into ctx.wsEndpoints before plugin setupPost, and createServer() starts from that same live endpoint map on Bun or Node. The runtime only changes the host transport implementation underneath.

The same split now applies to registry-backed event delivery. createApp() assembles ctx.events and freezes the bootstrap contract; createServer() is the host boundary that turns those event definitions into live SSE subscriptions, websocket fan-out, and graceful shutdown publishing. Event exposure and scope stay owned by the registry, while the runtime package only owns the transport.

Root ws is typed as the same WsConfig shape on both APIs. createApp() only carries that bootstrap config far enough for plugin wiring and context setup, while createServer() additionally owns transport startup, upgrade handling, and host-specific websocket wiring. That split lets tests exercise plugin WebSocket registration without opening a listener, while Bun and Node still consume the exact same endpoint draft when the server boots.

Switching from Bun to Node requires one line:

import { bunRuntime } from '@lastshotlabs/slingshot-runtime-bun';
import { nodeRuntime } from '@lastshotlabs/slingshot-runtime-node';
const runtime = bunRuntime();
const migratedRuntime = nodeRuntime();

Switching from Bun or Node to a worker host typically means replacing the runtime import and moving server bootstrap into a fetch entry module:

import { edgeRuntime } from '@lastshotlabs/slingshot-runtime-edge';
const runtime = edgeRuntime();

Plugins, routes, database adapters, and event bus contracts remain the same. The changes are in the host boundary, not the domain packages.

Need a host beyond the built-in Bun, Node, or edge packages? Implement SlingshotRuntime from @lastshotlabs/slingshot-core:

import type {
RuntimeServerInstance,
RuntimeServerOptions,
RuntimeSqliteDatabase,
SlingshotRuntime,
} from '@lastshotlabs/slingshot-core';
const sqliteDb = {} as RuntimeSqliteDatabase;
const myRuntime: SlingshotRuntime = {
password: {
hash: async plain => `hashed:${plain}`,
verify: async (plain, hash) => hash === `hashed:${plain}`,
},
sqlite: {
open: _path => sqliteDb,
},
server: {
listen: async (_opts: RuntimeServerOptions): Promise<RuntimeServerInstance> => ({
port: 3000,
stop() {},
}),
},
fs: {
write: async (_path, _data) => {},
readFile: async _path => null,
exists: async _path => false,
},
glob: {
scan: async (_pattern, _opts) => [],
},
readFile: async _path => null,
supportsAsyncLocalStorage: false,
};