Database & Schema
schema: { posts },
database: { adapter: bunSql() }Database, auth, CRUD, storage, jobs, email, and realtime are keys on one object, and bun run dev starts all of it.
bun add bunderstackEvery facility is a key, not a service to stand up. Read the file top to bottom and you know the system.
import { bunderstack } from 'bunderstack'
import { bunSql } from 'bunderstack/bun-sql'
import { pgTable, text } from 'drizzle-orm/pg-core'
import * as v from 'valibot'
const posts = pgTable('posts', {
id: text('id').primaryKey(),
title: text('title').notNull(),
userId: text('userId').notNull(),
})
export const backend = bunderstack({
schema: { posts },
database: { adapter: bunSql() },
auth: { secret: process.env.AUTH_SECRET! },
access: { posts: { ownerColumn: 'userId' } },
env: { client: { PUBLIC_APP_NAME: v.optional(v.string(), 'Example') } },
storage: { local: true, buckets: { images: { transforms: true } } },
email: { from: '[email protected]' },
realtime: true,
jobs: (j) =>
j.define({
digest: j.cron({
schedule: '0 9 * * *',
handler: async (_run, ctx: JobContext<{
posts: PgTableWithColumns<{
name: "posts";
schema: undefined;
columns: {
id: PgColumn<{
name: "id";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: true;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
title: PgColumn<{
name: "title";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [...];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
userId: PgColumn<...>;
};
dialect: "pg";
}>;
}, BaseEnv & {
...;
}>
ctx) => {
await ctx: JobContext<{
posts: PgTableWithColumns<{
name: "posts";
schema: undefined;
columns: {
id: PgColumn<{
name: "id";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: true;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
title: PgColumn<{
name: "title";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [...];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
userId: PgColumn<...>;
};
dialect: "pg";
}>;
}, BaseEnv & {
...;
}>
ctx.email.send({
to: '[email protected]',
subject: ctx: JobContext<{
posts: PgTableWithColumns<{
name: "posts";
schema: undefined;
columns: {
id: PgColumn<{
name: "id";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: true;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
title: PgColumn<{
name: "title";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [...];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
userId: PgColumn<...>;
};
dialect: "pg";
}>;
}, BaseEnv & {
...;
}>
ctx.env.PUBLIC_APP_NAME,
text: 'Daily digest',
})
},
}),
}),
api: (o) => ({
stats: o.protected.handler(async ({ context: Omit<ApiContext<{
posts: PgTableWithColumns<{
name: "posts";
schema: undefined;
columns: {
id: PgColumn<{
name: "id";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: true;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
title: PgColumn<{
name: "title";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [...];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
userId: PgColumn<...>;
};
dialect: "pg";
}>;
}, BaseEnv & {
...;
}> & object, "user" | "session"> & Omit<...> & {
...;
}
context }) => ({
total: 12,
requestedBy: context: Omit<ApiContext<{
posts: PgTableWithColumns<{
name: "posts";
schema: undefined;
columns: {
id: PgColumn<{
name: "id";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: true;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
title: PgColumn<{
name: "title";
tableName: "posts";
dataType: "string";
columnType: "PgText";
data: string;
driverParam: string;
notNull: true;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [...];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
userId: PgColumn<...>;
};
dialect: "pg";
}>;
}, BaseEnv & {
...;
}> & object, "user" | "session"> & Omit<...> & {
...;
}
context.user.id,
})),
}),
})
export type App = Awaited<ReturnType<typeof backend.start>>src/bunderstack.tsImport type App and stop. Tables, procedures, inputs, outputs, and errors are inferred from the declaration.
import { QueryClient } from '@tanstack/react-query'
import { createClient } from 'bunderstack/query'
import { createAuthClient } from 'better-auth/react'
import type { App } from './bunderstack'
export const queryClient = new QueryClient()
export const api = createClient<App>({ queryClient })
export const authClient = createAuthClient()
const result = await api.stats.call()src/api-client.tsQueries, mutations, realtime subscriptions, and storage helpers in your React components with full inference.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { syncRealtime } from 'bunderstack/query'
import { useEffect, useState } from 'react'
import { api, authClient } from './api-client'
export function Feed() {
const queryClient = useQueryClient()
const { data: const session: {
user: StripEmptyObjects<{
id: string;
createdAt: Date;
updatedAt: Date;
email: string;
emailVerified: boolean;
name: string;
image?: string | null | undefined;
}>;
session: StripEmptyObjects<{
id: string;
createdAt: Date;
updatedAt: Date;
userId: string;
expiresAt: Date;
token: string;
ipAddress?: string | null | undefined;
userAgent?: string | null | undefined;
}>;
} | null
session } = authClient.useSession()
const [title, setTitle] = useState('')
const [file, setFile] = useState<File | null>(null)
const { data: const posts: {
items: {
id: string;
title: string;
userId: string;
}[];
nextCursor?: string | undefined;
hasMore: boolean;
total?: number | undefined;
limit?: number | undefined;
offset?: number | undefined;
cursor?: string | undefined;
q?: string | undefined;
sort?: string | undefined;
order?: string | undefined;
} | undefined
The last successfully resolved data for the query.posts } = useQuery(api.posts: {
create: Public<ProcedureUtils<object, Partial<{
id: string;
title: string;
userId: string;
}>, {
id: string;
title: string;
userId: string;
}, Error | ORPCErrorFromErrorMap<{
readonly BAD_REQUEST: {
readonly data: OptionalSchema<StrictObjectSchema<{
readonly code: OptionalSchema<LiteralSchema<"BAD_REQUEST", undefined>, "BAD_REQUEST">;
readonly details: OptionalSchema<UnknownSchema, undefined>;
}, undefined>, {
readonly code: "BAD_REQUEST";
}>;
};
readonly UNAUTHORIZED: {
...;
};
... 4 more ...;
readonly TOO_MANY_REQUESTS: {
...;
};
}>>>;
... 4 more ...;
live: Public<...>;
} & Public<...>
posts.list.queryOptions())
const { data: const stats: {
total: number;
requestedBy: string;
} | undefined
The last successfully resolved data for the query.stats } = useQuery(api.stats: Public<ProcedureUtils<object, void, {
total: number;
requestedBy: string;
}, Error | ORPCErrorFromErrorMap<{
readonly BAD_REQUEST: {
readonly data: OptionalSchema<StrictObjectSchema<{
readonly code: OptionalSchema<LiteralSchema<"BAD_REQUEST", undefined>, "BAD_REQUEST">;
readonly details: OptionalSchema<UnknownSchema, undefined>;
}, undefined>, {
readonly code: "BAD_REQUEST";
}>;
};
readonly UNAUTHORIZED: {
...;
};
... 4 more ...;
readonly TOO_MANY_REQUESTS: {
...;
};
}>>>
stats.queryOptions())
const const createPost: UseMutationResult<{
id: string;
title: string;
userId: string;
}, Error | ORPCError<"BAD_REQUEST", {
code: "BAD_REQUEST";
details?: unknown;
}> | ORPCError<"UNAUTHORIZED", {
code: "UNAUTHORIZED";
details?: unknown;
}> | ORPCError<"FORBIDDEN", {
code: "FORBIDDEN";
details?: unknown;
}> | ORPCError<"NOT_FOUND", {
code: "NOT_FOUND";
details?: unknown;
}> | ORPCError<"CONFLICT", {
code: "CONFLICT";
details?: unknown;
}> | ORPCError<"PAYLOAD_TOO_LARGE", {
code: "PAYLOAD_TOO_LARGE";
details?: unknown;
}> | ORPCError<"TOO_MANY_REQUESTS", {
code: "TOO_MANY_REQUESTS";
details?: unknown;
}>, Partial<...>, unknown>
createPost = useMutation(api.posts: {
create: Public<ProcedureUtils<object, Partial<{
id: string;
title: string;
userId: string;
}>, {
id: string;
title: string;
userId: string;
}, Error | ORPCErrorFromErrorMap<{
readonly BAD_REQUEST: {
readonly data: OptionalSchema<StrictObjectSchema<{
readonly code: OptionalSchema<LiteralSchema<"BAD_REQUEST", undefined>, "BAD_REQUEST">;
readonly details: OptionalSchema<UnknownSchema, undefined>;
}, undefined>, {
readonly code: "BAD_REQUEST";
}>;
};
readonly UNAUTHORIZED: {
...;
};
... 4 more ...;
readonly TOO_MANY_REQUESTS: {
...;
};
}>>>;
... 4 more ...;
live: Public<...>;
} & Public<...>
posts.create.mutationOptions())
useEffect(() => {
const live = syncRealtime({ api, queryClient, tables: ['posts'] })
return () => live.close()
}, [queryClient])
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (file) await api.files.images.upload(file)
const createPost: UseMutationResult<{
id: string;
title: string;
userId: string;
}, Error | ORPCError<"BAD_REQUEST", {
code: "BAD_REQUEST";
details?: unknown;
}> | ORPCError<"UNAUTHORIZED", {
code: "UNAUTHORIZED";
details?: unknown;
}> | ORPCError<"FORBIDDEN", {
code: "FORBIDDEN";
details?: unknown;
}> | ORPCError<"NOT_FOUND", {
code: "NOT_FOUND";
details?: unknown;
}> | ORPCError<"CONFLICT", {
code: "CONFLICT";
details?: unknown;
}> | ORPCError<"PAYLOAD_TOO_LARGE", {
code: "PAYLOAD_TOO_LARGE";
details?: unknown;
}> | ORPCError<"TOO_MANY_REQUESTS", {
code: "TOO_MANY_REQUESTS";
details?: unknown;
}>, Partial<...>, unknown>
createPost.mutate({ title })
setTitle('')
}
return (
<div>
<header>
<span>Signed in as {const session: {
user: StripEmptyObjects<{
id: string;
createdAt: Date;
updatedAt: Date;
email: string;
emailVerified: boolean;
name: string;
image?: string | null | undefined;
}>;
session: StripEmptyObjects<{
id: string;
createdAt: Date;
updatedAt: Date;
userId: string;
expiresAt: Date;
token: string;
ipAddress?: string | null | undefined;
userAgent?: string | null | undefined;
}>;
} | null
session?.user.name}</span>
<h2>Posts ({const stats: {
total: number;
requestedBy: string;
} | undefined
The last successfully resolved data for the query.stats?.total ?? 0})</h2>
</header>
<form onSubmit={onSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="New post"
/>
<input
type="file"
accept="image/*"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
<button type="submit">Publish</button>
</form>
<ul>
{const posts: {
items: {
id: string;
title: string;
userId: string;
}[];
nextCursor?: string | undefined;
hasMore: boolean;
total?: number | undefined;
limit?: number | undefined;
offset?: number | undefined;
cursor?: string | undefined;
q?: string | undefined;
sort?: string | undefined;
order?: string | undefined;
} | undefined
The last successfully resolved data for the query.posts?.items.map((post: {
id: string;
title: string;
userId: string;
}
post) => ( <li key={post: {
id: string;
title: string;
userId: string;
}
post.id}>
<span>{post: {
id: string;
title: string;
userId: string;
}
post.title}</span>
<img src={api.files.images.url(post: {
id: string;
title: string;
userId: string;
}
post.id, { w: 32, h: 32 })} />
</li>
))}
</ul>
</div>
)
}src/Feed.tsxEach of these is useful alone. Together they remove the adapters and lifecycle code that normally fill the space between them.
schema: { posts },
database: { adapter: bunSql() }auth: {
secret: process.env.AUTH_SECRET!
}access: {
posts: { ownerColumn: 'userId' }
}api: (o) => ({
stats: o.protected.handler(async ({ context }) => ({
total: 12,
requestedBy: context.user.id,
})),
})storage: {
local: true,
buckets: { images: { transforms: true } }
}email: {
from: '[email protected]'
}realtime: truejobs: (j) =>
j.define({
digest: j.cron({
schedule: '0 9 * * *',
handler: async (_run, ctx) => {
await ctx.email.send({ ... })
},
}),
})Application-sized tests of the mental model, not isolated feature demos.
examples/todoCustom procedures, generated CRUD, auth, storage, jobs, and realtime in one readable app.
Collaborative canvasexamples/tldrawLive cursors and shared drawings with typed presence and resilient reconnect behavior.
Realtime product UIexamples/kanban-tanstackTanStack Start, Query, and generated API procedures across boards, cards, and comments.
Growing live collectionsexamples/twitter-db-tanstackA feed built on scoped TanStack DB collections, cursor windows, and typed updates.