Quickback Docs

quickback-provider

QuickbackProvider plus TanStack Query hooks and role helpers over the emitted client — the base every other item builds on.

npx shadcn add @quickback/quickback-provider

Installs components/quickback-provider.tsx and @tanstack/react-query@^5. The other items import their hooks from it.

<QuickbackProvider>

import { createClient } from "@/lib/quickback.client"
import { QuickbackProvider } from "@/components/quickback-provider"

const client = createClient({ baseUrl: "https://api.example.com" })

<QuickbackProvider client={client} roles={roles}>
  <App />
</QuickbackProvider>
PropTypeDescription
clientClientThe result of createClient() from the emitted @/lib/quickback.client. Required.
rolesreadonly string[]The signed-in caller's roles, e.g. the active organization member role from Better Auth. A UI hint only. See Roles.
realtime{ subscribe(handler) => unsubscribe }Source of realtime frames for useLive. Defaults to client.realtime when the emitted client has one.
queryClientQueryClientShare your app's TanStack Query cache. Omit it and the provider creates one that retries network errors and 5xx twice and never retries a 4xx.
childrenReactNode

Hooks

Every hook takes a schema-registry table name ("jobs"). The name is resolved through the client's resources table, so generic UI never hard-codes a URL. An unknown name throws Unknown Quickback resource "<name>". Calling a verb the table has no route for throws "<table>" has no <verb> route.

HookReturnsNotes
useQuickback()ClientThe typed client. Use it for anything the hooks don't cover.
useRoles()readonly string[] | undefinedThe roles passed to the provider.
useSchema()query of SchemaRegistryGET /api/v1/schema. Cached for the session.
useTableMeta(table)TableMeta | undefinedOne table from the registry. undefined while the registry loads.
useOps(table)string[]The operations that have a route (list, get, create, …).
useList(table, params?)query of { data, pagination }params takes list params, plus view to read a named view and enabled. Keeps the previous page while the next one loads.
useInfiniteList(table, params?)infinite queryPages with the keyset cursor (nextCursor → startingAfter). Falls back to offset when the server returns no cursor, for example when sorting on a masked column.
useRecord(table, id)query of the recordDisabled while id is undefined.
useCreate(table)mutation (body) => row
useUpdate(table)mutation ({ id, body }) => row
useDelete(table)mutation (id) => void
useAction(table, action)mutation ({ id?, params?, input }) => outputRecord-bound actions need id. Standalone actions with path parameters need params.
useLive(table, enabled = true)booleanRefetches the table when a postgres_changes frame for it arrives. Returns whether a realtime source is wired up.
useDebouncedValue(value, ms = 250)Tvalue, updated only after it stops changing for ms.

Every mutation invalidates all of the table's cached queries.

import { useList, useUpdate } from "@/components/quickback-provider"

function OpenJobs() {
  const jobs = useList("jobs", { filter: { status: "open" }, sort: "-createdAt", limit: 10 })
  const update = useUpdate("jobs")
  if (jobs.isPending) return <p>Loading…</p>
  return jobs.data?.data.map((job) => (
    <button key={String(job.id)} onClick={() => update.mutate({ id: String(job.id), body: { status: "closed" } })}>
      Close {String(job.title)}
    </button>
  ))
}

The hooks are deliberately untyped by name (Row = Record<string, unknown>), because they serve generic UI. For typed rows, call the client directly: useQuickback().jobs.list() returns Api.jobs.Row[].

Roles

The emitted client has no session method, so the provider only knows the caller's roles if you pass them. Read the role from your auth client:

const member = auth.useActiveMember() // better-auth organizationClient()
<QuickbackProvider client={client} roles={member.data ? [member.data.role] : undefined}>

Roles are a UI hint. The API stays authoritative:

  • Masking. A masked column is read-only unless the roles satisfy its show rule. With no roles, every masked column is read-only. A value that arrives masked ([REDACTED], j***@…) is always read-only.
  • Access. When the roles plainly fail an operation's access.roles, the form's Delete button is hidden and the action dialog shows a role message instead of its form. The client can't decide hierarchy (member+), record, or FGA rules, so those controls stay available and the API answers.

Realtime

useLive(table) refetches the table on its change frames. ResourceTable calls it unless live={false}. The frames come from client.realtime when the emitted client has one, which it does when the project enables realtime. Otherwise pass any { subscribe(handler) => unsubscribe } source as realtime. See Using Realtime for the ticket and WebSocket handshake.

Helpers

The other items use these, and you can too:

ExportDescription
describeError(error){ message, fields, status? }. message is the problem's detail, then its title, then error.message. fields maps field names to messages, from QuickbackError.fields.
isMasked(meta, column, roles, value?)Whether the column may reach these roles masked.
canAccess(access, roles)Best-effort role check. Returns true for rules the client can't decide.
canWrite(meta, op, roles)canAccess for create / update / delete from the registry.
getDisplayColumn(tableMeta)The column that names a record: the table's displayColumn, else the first of name, title, label, …
formatLabel(name) / columnLabel(name, col?)salaryMin → Salary Min. An FK column reads as its target: companyId → Company.
REDACTED"[REDACTED]", the value the API sends in place of a redacted value.

Types: TableMeta, ColumnMeta, ActionMeta (all derived from the emitted SchemaRegistry), Row, Page, RealtimeSource, and RealtimeFrame.

On this page