bunderstackbunderstack/ docs

Getting Started

Build and call a typed Bunderstack API in five minutes

Install

bun add bunderstack drizzle-orm valibot @libsql/client @tanstack/react-query
bun add -d drizzle-kit

Define a schema

Use the Drizzle builders directly and re-export Bunderstack's internal tables so storage and runtime metadata are included in migrations.

// schema.ts
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'

export * from 'bunderstack/schema'

export const posts = sqliteTable('posts', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  userId: text('userId').notNull(),
  createdAt: integer('createdAt', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
})

Better Auth also needs its user, session, account, and verification tables. Copy the matching SQLite or Postgres definitions from an example or the SaaS template.

Create the app

The custom procedure below joins the same graph as generated CRUD. Valibot is used for runtime input validation; the handler return type becomes the output contract automatically.

// bunderstack.ts
import { bunderstack } from 'bunderstack'
import { libsql } from 'bunderstack/libsql'
import { provision } from 'bunderstack/provision'
import { count } from 'drizzle-orm'
import * as v from 'valibot'

import { posts } from './schema'
import * as schema from './schema'

export const backend = bunderstack({
  schema,
  database: { adapter: libsql(), url: 'file:./data.db' },
  access: {
    posts: {
      ownerColumn: 'userId',
      searchableColumns: ['title'],
      sortableColumns: ['createdAt', 'id'],
    },
  },
  realtime: true,
  api: (o) => ({
    postCount: o.public
      .input(v.optional(v.object({})))
      .handler(async ({ context }) => {
        const [row] = await context.db.select({ value: count() }).from(posts)
        return { value: row?.value ?? 0 }
      }),
  }),
})

export const app = await backend.start()

export type App = typeof app

await provision(app)

provision(app) pushes the schema while there is no migrations journal. Once you run bunx drizzle-kit generate and commit the migrations, the same call only applies pending migrations. Skip it when deployment manages migrations outside the application.

Serve one handler

// server.ts
import { app } from './bunderstack'

Bun.serve({ fetch: app.handler })

Generated procedures are callable through typed RPC and ordinary HTTP:

GET    /api/posts
POST   /api/posts
PATCH  /api/posts/:id
DELETE /api/posts/:id
POST   /api/rpc/postCount
GET    /api/openapi.json       when openapi: true

Call it from the client

Only the App type crosses the boundary; server code is not bundled.

// api-client.ts
import { QueryClient } from '@tanstack/react-query'
import { createClient } from 'bunderstack/query'
import type { App } from './bunderstack'

export const queryClient = new QueryClient()
export const api = createClient<App>({ queryClient })

const page = await api.posts.list.call({ limit: 20 })
const total = await api.postCount.call({})
// page and total are inferred from the server graph

Database choices

Import exactly one adapter. Its dialect must match your Drizzle tables.

DatabaseAdapterOptional peer
SQLite / Tursobunderstack/libsql@libsql/client
Embedded Postgresbunderstack/pglite@electric-sql/pglite
Postgres on Bunbunderstack/bun-sqlnone
Postgres on Nodebunderstack/postgres-jspostgres

Continue with Auto CRUD, then add application behavior in API Procedures.

On this page