useQuery reference
The exact return type, REST contract, WebSocket realtime behaviour, and reconnect strategy of @omg-dev/sdk's useQuery hook (and the deprecated useCollection alias).
useCollectionis deprecated — useuseQueryinstead.useQueryis the WebSocket-first default and adds an optionalwherepredicate that scopes rows on the server.useCollection({ api, collection })still works (it now forwards touseQuery), but new code should useuseQuery.
useQuery<T> owns the realtime subscription, the initial snapshot, and the
mutation primitives. By default it subscribes over a WebSocket
(/__vibes_sub) and receives a full snapshot plus live row deltas; it falls
back to the legacy SSE+REST loop only if the WebSocket endpoint is unreachable.
Signature
import { useQuery } from "@omg-dev/sdk"
interface UseQueryOptions {
collection: string // e.g. "tasks"
where?: SubscribePredicate // optional server-side row filter
api?: string // e.g. "/api/tasks" — enables create/update/remove + legacy fallback
}
interface UseQueryReturn<T> {
data: T[]
loading: boolean
error: string | null
create: (item: Partial<T>) => Promise<T>
update: (id: string, patch: Partial<T>) => Promise<T>
remove: (id: string) => Promise<void>
refresh: () => void
}
useQuery<T extends { id: string }>(opts: UseQueryOptions): UseQueryReturn<T>where accepts a predicate AST ({ op: "eq", column: "done", value: false },
{ op: "and", clauses: [...] }, etc.) and is evaluated server-side against the
snapshot and every delta, so a subscriber only ever receives matching rows.
data is initialised to [] on the very first render — data.map(...) is
safe before the first fetch resolves. The hook never returns undefined for
data, even on error.
Canonical use
import { useState } from "react"
import { useQuery } from "@omg-dev/sdk"
interface Task { id: string; title: string; done: boolean }
export default function Tasks() {
const { data, loading, error, create, update, remove } = useQuery<Task>({
api: "/api/tasks",
collection: "tasks",
// optional: only open tasks → where: { op: "eq", column: "done", value: false },
})
const [draft, setDraft] = useState("")
if (error) return <p>Failed: {error}</p>
return (
<div>
<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>
{loading && data.length === 0 ? <p>Loading...</p> : null}
<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>
</div>
)
}REST contract
The hook talks to four endpoints, all relative to opts.api:
| Method | Path | Body | Response |
|---|---|---|---|
GET | <api> | — | T[] |
POST | <api> | Partial<T> (JSON) | T |
PATCH | <api>/:id | Partial<T> (JSON) | T |
DELETE | <api>/:id | — | 204 (body ignored) |
opts.collection does NOT affect any of these URLs — it's only used to
filter SSE events. Convention: pass the same string as the path segment in
api (e.g. api: "/api/tasks", collection: "tasks").
The runtime mounts these handlers automatically for every collection in
schema.ts. If a collection is .scoped("user"), list/get filter by
_owner = currentUser and create stamps _owner on insert.
Realtime: WebSocket first, SSE fallback
By default the hook subscribes over a WebSocket (/__vibes_sub): it gets a full
snapshot on subscribe and then per-row insert/update/delete deltas — no
re-fetch per change. The SSE+REST loop described below is the fallback, used
only when the WebSocket endpoint can't be reached (e.g. a backend deployed
before the WS engine existed). On that path:
After the first successful fetch the hook opens an EventSource against
/__vibes_events. Every message is parsed as JSON; only events of the
shape { type: "invalidate", collection: <opts.collection> } trigger a
re-fetch. Anything else is dropped.
The runtime broadcasts { type: "invalidate", collection } on every
successful create/update/remove for that collection — across every
connected client. That means two browser tabs viewing the same deploy
stay in sync without any per-client polling.
Reconnect strategy
Bounded so a permanently-broken endpoint (deleted deploy, gone host) can't
turn into a per-tab DDoS. From packages/sdk/src/index.ts:
- On
error: close, double the delay (1s, 2s, 4s, 8s, 16s, 32s; cap 60s). - After 6 consecutive errors with no successful message in between, the hook gives up — reload the page to retry.
- Each successful message resets the counter.
Don't remount the component to "fix" a dead stream; it just hits the give-up logic again.
Mutation responses
create and update return the server's JSON response. The hook does
not optimistically update data — it relies on the SSE invalidation to
re-fetch, which is fast on a warm runtime VM. For optimistic UI, hold it in
component state and reconcile on the next data. remove returns void.
Auth and scope
The runtime verifies the Authorization: Bearer <jwt> on every request via
createAuthMiddleware("vibes"). If missing or invalid, the request runs as
userId = "anonymous". For .scoped("user") collections, the row-level
filter then reduces to _owner = "anonymous", which is fine for unscoped
data but means anonymous and signed-in users see different rows.
The platform's web app injects an auth-scoped token into every fetch from a deployed page. You don't need to wire this manually.
Common errors
| Browser error | Real cause | Fix |
|---|---|---|
TypeError: r.map is not a function | Code wrote const tasks = useCollection(...) and then tasks.map(...). The hook returns the full options object, not the array. | const { data } = useCollection(...), then data.map(...). |
Cannot read properties of undefined (reading 'map') | Same root cause as above. The hook NEVER returns undefined data; if data.map throws on undefined, you're calling it on the wrong value. | Same fix. |
SyntaxError: Unexpected token 'm', "method not allowed" ... is not valid JSON | The deploy classified as static. The orchestrator skipped the runtime VM, so POST /api/<collection> is unrouted; the static asset proxy returns the literal text method not allowed, and the SDK's JSON.parse chokes on m. | Fix schema.ts so it default-exports a Schema with at least one collection. See Schema reference. |
EventSource connection failed (silent, no more updates) | Six consecutive SSE failures — the give-up threshold. | Reload the page. If it persists, the deploy or the host is down. |
What the hook does NOT do
- No optimistic mutations. Use component state.
- No pagination, sorting, or filtering helpers. Use a custom
functions/<name>.tsroute and call it viafetch. - No client-side caching across components. Two
useCollectioncalls for the same collection issue twoGETs. They both invalidate on the same SSE event, but they don't share state. - No retry on mutation failure.
create/update/removereject the promise; handle in yourawaitsite. - No type-narrowing on
error. It'sstring | null, the JS error message verbatim.
Where the source lives
packages/sdk/src/index.ts is the entire implementation — under 150 lines.
If a behaviour isn't documented here, the file is short enough to read end
to end.