kastor0.1.0
Get started

GUIDE 32Bun

kastor with Bun

Bun ships SQLite in the runtime, loads .env by itself and serves HTTP without a framework. kastor takes a bun:sqlite database directly, so a typed data layer needs no driver install at all, and Postgres or MySQL work through the usual drivers.

Project structure

A Bun server can be a single file, but a database deserves its own module. With SQLite, the database is a file next to the app.

Project
src/
  db/schema.ts            tables and relations
  db/index.ts             the client, from bun:sqlite
  server.ts               Bun.serve and routes
kastor.config.ts          for the CLI, through better-sqlite3
kastor/migrations/
data/app.db               the SQLite file
.env                      loaded by Bun automatically
Terminal
bun add kastor-orm
bun add -d better-sqlite3
bunx kastor-orm init --db sqlite

The client from bun:sqlite

Pass a bun:sqlite Database to the sqlite adapter. kastor turns on foreign key enforcement, which SQLite leaves off by default, and WAL mode is worth enabling for concurrent readers. bun --hot keeps the process alive and reloads modules, so store the client on globalThis to reuse one handle.

src/db/index.ts
import { Database } from 'bun:sqlite'
import { kastor } from 'kastor-orm'
import { sqlite } from 'kastor-orm/sqlite'
import { schema, relations } from './schema'

const create = () => {
  const file = new Database(process.env.DB_FILE ?? 'data/app.db', { create: true })
  file.exec('PRAGMA journal_mode = WAL')
  return kastor({ adapter: sqlite(file), schema, relations })
}

const g = globalThis as unknown as { db?: ReturnType<typeof create> }
export const db = g.db ?? create()
g.db = db

For Postgres, swap the adapter: postgres(new pg.Pool({ connectionString: process.env.DATABASE_URL })) with bun add pg. Everything else in this guide stays the same.

Serving routes with Bun.serve

Bun's built in router matches paths and passes parameters on req.params. Each handler queries kastor and returns a Response.

src/server.ts
import { isKastorError } from 'kastor-orm'
import { db } from './db'

const server = Bun.serve({
  port: Number(process.env.PORT ?? 3000),
  routes: {
    '/posts': {
      GET: async () =>
        Response.json(await db.posts.findMany({ where: { published: true }, include: { author: true }, take: 20 })),
      POST: async (req: Request) => {
        const body = (await req.json()) as { title: string; authorId: number }
        return Response.json(await db.posts.create({ data: body }), { status: 201 })
      },
    },
    '/posts/:id': async (req: Request & { params: { id: string } }) => {
      const post = await db.posts.findUnique({ where: { id: Number(req.params.id) }, include: { tags: true } })
      return post ? Response.json(post) : new Response('Not found', { status: 404 })
    },
  },
  error(err: unknown) {
    if (isKastorError(err)) return Response.json({ error: err.code, message: err.message }, { status: 400 })
    return new Response('Internal error', { status: 500 })
  },
})

console.log(`listening on ${server.url}`)

Run it with bun --hot src/server.ts in development. Bun reads .env, .env.local and .env.production on its own, so process.env.DB_FILE or DATABASE_URL are available without a loader.

Migrations with the CLI

The kastor CLI runs on Node.js, which cannot import bun:sqlite. Give the CLI its own adapter for the same file through better-sqlite3, installed as a dev dependency above. bunx runs the CLI with Node when Node is installed, as it is on most CI images.

kastor.config.ts
import Database from 'better-sqlite3'
import { defineConfig } from 'kastor-orm'
import { sqlite } from 'kastor-orm/sqlite'

export default defineConfig({
  schema: './src/db/schema.ts',
  adapter: () => sqlite(new Database(process.env.DB_FILE ?? 'data/app.db')),
  migrations: './kastor/migrations',
})
Terminal
bunx kastor migrate dev --name init
bunx kastor migrate deploy
bun run src/server.ts

Run kastor migrate deploy before the server starts, against the same file the server will open. With Postgres, the config uses pg like any Node project and nothing Bun specific is involved.

Deploying a Bun app with SQLite

SQLite lives on disk, so the server needs a persistent volume and a single writer. That fits a single machine or a container with a mounted volume. If you need several instances, use Postgres, or Turso with the libsql adapter, which works on Bun as well.

Dockerfile
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
VOLUME /app/data
CMD ["bun", "run", "src/server.ts"]

The official Bun image has no Node.js, and the kastor CLI needs it. Run migrations as a release step instead: a job with Node that mounts the same volume and runs npx kastor migrate deploy before the new container starts. With Postgres, that step is an ordinary CI job with DATABASE_URL. If you prefer migrating inside the container, start from an image that has both runtimes.

Testing with bun test

bun test runs test files with the same runtime, so tests can open an in memory database with new Database(':memory:') from bun:sqlite, create the schema by running the migration SQL, and use the same kastor client as the app. Each test file gets a fresh database in milliseconds, which keeps tests independent without cleanup code.

Production checklist

Before the first deploy, check these points. Each one prevents a problem that otherwise shows up only under real traffic, usually as exhausted connections, a leaked secret or a schema that does not match the code that is running:

  • The client sits on globalThis, so bun --hot never opens a second handle.
  • The SQLite file lives on a persistent volume, outside the image.
  • The CLI config uses better-sqlite3 or Postgres, which Node can load.
  • Migrations run in a release step with Node before the new server starts.
  • WAL mode is on for SQLite, so readers do not block the writer.

Pitfalls

  • Two drivers, one file. The app uses bun:sqlite and the CLI uses better-sqlite3 on the same file. Avoid running migrations while the app writes; stop it or migrate before start.
  • Interactive transactions. SQLite allows one writer at a time. Keep $transaction callbacks short so other requests are not blocked.
  • Booleans and dates. SQLite has no boolean or date types; kastor decodes t.boolean() and t.datetime() for you. Raw $sql results come back as stored, as 0 and 1 and ISO strings.
  • bun --hot and pools. Without the globalThis guard, every hot reload opens another handle or pool.

Questions people ask

Q1Does kastor support bun:sqlite?

Yes. The sqlite adapter from kastor-orm/sqlite detects a bun:sqlite Database, as well as better-sqlite3 and node:sqlite, and uses the same SQLite dialect for all three.

Q2Can I use Postgres with kastor on Bun?

Yes. Bun runs the pg and mysql2 packages, so the postgres and mysql adapters work the same way they do on Node.

Q3Why does the kastor CLI need a different SQLite driver?

The CLI is a Node.js program, and Node cannot load bun:sqlite. Point kastor.config.ts at the same database file through better-sqlite3 (or node:sqlite), and the app keeps using bun:sqlite at runtime.

Q4How do I avoid reopening the database with bun --hot?

bun --hot reloads modules in the same process. Keep the client on globalThis so reloads reuse it instead of opening another handle.

Suggestions

Kas looked under every log. Nothing matches that yet.

Try a method name, a database or a framework.