SDK overview
Building DB-backed apps with @omg-dev/schema, @omg-dev/sdk, and @omg-dev/server.
The Vibes SDK is three packages cooperating across one project:
| Package | Where it runs | What it does |
|---|---|---|
@omg-dev/schema | Build time | Declarative schema; emits SQL DDL, TS types, Zod, and the build manifest's hasDb flag. |
@omg-dev/sdk | Browser | useQuery<T> — WebSocket snapshot + live row deltas, optional where scope, and mutations (useCollection is the deprecated alias). |
@omg-dev/server | Bun runtime | Mounts /api/<collection> against sqlite, dispatches functions/*.ts, broadcasts invalidations. |
The pipeline keys off one file: schema.ts at project root. Find
default.collections and the deploy classifies as runtime — a Firecracker
VM serves /api/*. Otherwise it's static, the VM is skipped, and mutations
return method not allowed. Getting that file shape right is the single
load-bearing step; see Schema reference for the contract.
Minimum viable DB-backed app
The fresh sandbox at /home/user/project already has Vite, React, Tailwind,
shadcn, and @omg-dev/* installed. To turn it into a DB app, write two files.
1. schema.ts (project root, default-exported)
import { defineSchema, collection, fields } from "@omg-dev/schema"
export default defineSchema({
collections: {
tasks: collection({
fields: {
title: fields.string(),
done: fields.boolean(),
},
}),
},
})This file MUST be at the project root, not under src/ — the build does
await import("./schema.ts") from process.cwd(). Anywhere else silently
ships a static deploy.
2. src/App.tsx
import { useState } from "react"
import { useQuery } from "@omg-dev/sdk"
interface Task { id: string; title: string; done: boolean }
export default function App() {
const { data, create, update, remove } = useQuery<Task>({
api: "/api/tasks",
collection: "tasks",
})
const [draft, setDraft] = useState("")
return (
<main>
<h1>Tasks</h1>
<form
onSubmit={async (e) => {
e.preventDefault()
if (!draft.trim()) return
await create({ title: draft, done: false })
setDraft("")
}}
>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
<button type="submit">Add</button>
</form>
<ul>
{data.map((t) => (
<li key={t.id}>
<input
type="checkbox"
checked={t.done}
onChange={() => update(t.id, { done: !t.done })}
/>
<span>{t.title}</span>
<button onClick={() => remove(t.id)}>x</button>
</li>
))}
</ul>
</main>
)
}That is the whole DB app. No useEffect, no fetch, no DB client, no auth wiring.
data is always an array (defaults to [] before the first fetch resolves) so
.map is safe from the first render.
What happens at deploy time
bun run buildruns the user-facing client build (Vite).- The orchestrator chains
vibes-build(@omg-dev/vite-plugin's bin), which:- Imports
schema.ts, readsdefault.collections, decideshasDb. - Scans
functions/*.ts, generates.vibes/routes.generated.ts. - Bundles everything into a single
.vibes/server.mjsviabun build --target=bun. - Writes
.vibes/artifacts.jsonwith{ static, hasDb, functions, ... }.
- Imports
- The orchestrator (
apps/infra/internal/orchestrator/manager.go) reads the manifest. If!hasDb && functions === 0it takes the static path (no VM). Otherwise it forks a runtime VM that runsbun .vibes/server.mjs. - At runtime,
@omg-dev/server:- Opens
.vibes/data.db(litestream-restored fromdeploys-db/<slug>/v:N-1/, replicates tov:N/). - Auto-mounts
/api/<collection>for every collection in the schema. - Dispatches
functions/*.tsexports as additional routes. - Streams invalidations on
/__vibes_eventsso every connected client refreshes when any mutation lands.
- Opens
Custom routes (functions)
useCollection covers the common CRUD shape. For anything else, drop a file
in functions/:
// functions/search.ts
export default {
POST: async (req: Request) => {
const { q } = await req.json()
return Response.json({ results: [] })
},
}Builds at /api/search. The build scanner detects every file in functions/,
codegens preloaded module imports, and ships them inside .vibes/server.mjs
— no node_modules/ lookup at runtime.
Common errors
| Browser console | Real cause | Fix |
|---|---|---|
TypeError: r.map is not a function | useCollection was destructured wrong (it returns an object, not an array). | const { data } = useCollection(...), then data.map(...). |
Unexpected token 'm', "method not allowed" ... | Static-only deploy. schema.ts didn't default-export collections, so the orchestrator dropped the runtime VM. | Verify schema.ts is at root and default-exports defineSchema(...). See Schema reference. |
Reference
- Schema reference —
defineSchema,collection, everyfields.*builder, scope, indexes. - useCollection reference — the hook's exact return type, API contract, and SSE behaviour.
- Architecture — how the request reaches the runtime VM.
- Deploy flow — what happens between
bun run buildand a live URL.