omg/docs
SDK

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).

useCollection is deprecated — use useQuery instead. useQuery is the WebSocket-first default and adds an optional where predicate that scopes rows on the server. useCollection({ api, collection }) still works (it now forwards to useQuery), but new code should use useQuery.

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:

MethodPathBodyResponse
GET<api>T[]
POST<api>Partial<T> (JSON)T
PATCH<api>/:idPartial<T> (JSON)T
DELETE<api>/:id204 (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 errorReal causeFix
TypeError: r.map is not a functionCode 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 JSONThe 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>.ts route and call it via fetch.
  • No client-side caching across components. Two useCollection calls for the same collection issue two GETs. They both invalidate on the same SSE event, but they don't share state.
  • No retry on mutation failure. create/update/remove reject the promise; handle in your await site.
  • No type-narrowing on error. It's string | 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.