Skip to content

Querying with the SDK

base.from(table) builds PostgREST requests. Every call resolves to { data, error, count, status, statusText }. Errors are returned, never thrown.

const { data, error, count } = await base
.from("products")
.select("id,name,price", { count: "estimated" })
.eq("active", true)
.order("price", { ascending: false })
.limit(20)

error is a BaseError with code (a PostgREST or Postgres code such as PGRST116 or 23505, or a Base code such as rate_limited), message, details, hint, request_id, status and retry_after.

Filters: eq, neq, gt, gte, lt, lte, like, ilike, is, in, contains, containedBy, overlaps, or, not, match, filter. Modifiers: order, limit, range, single, maybeSingle, select, abortSignal.

await base.from("todos").select("*").in("status", ["new", "in progress"])
await base.from("todos").select("*").contains("tags", ["urgent"])
await base.from("todos").select("*").or("priority.gt.3,and(done.is.false,due.is.null)")
await base.from("todos").select("*").not("owner", "is", null)
await base.from("todos").select("*", { count: "exact", head: true }) // count only
await base.from("todos").select("*").eq("id", 1).single() // error unless exactly one row
await base.from("todos").select("*").eq("id", 1).maybeSingle() // null when no row
const { data } = await base.from("authors").select("name, books(title, chapters(n))")

Embeds deeper than db.maxEmbedDepth (default 3) are rejected client-side with error.code = "embed_depth_exceeded" and no request is sent.

Writes return data: null unless you chain .select().

await base.from("products").insert({ name: "Tea", price: 3 })
await base.from("products").insert([{ name: "A", price: 1 }, { name: "B", price: 2 }]).select("id")
await base.from("products").upsert({ id: 1, name: "Tea" }, { onConflict: "id" })
await base.from("products").update({ price: 4 }).eq("id", 1).select().single()
await base.from("products").delete().eq("id", 1)
await base.rpc("checkout", { cart_id: cartId })
await base.rpc("search", { q: "tea" }, { get: true }) // GET, for STABLE functions
await base.rpc("open_orders", {}, { head: true, count: true })
await base.rpc("open_orders").gte("total", 10).order("total").select("id,total")

A function must be granted explicitly: grant execute on function public.checkout(uuid) to authenticated;. SECURITY DEFINER functions bypass RLS, so check ownership inside them and set search_path = ''.

Reads are deduplicated. Opt in to caching per query or globally:

const base = createClient({ url, key, cache: { staleTime: 30_000, gcTime: 300_000 } })
await base.from("todos").select().cache({ staleTime: 5_000 })

Writes invalidate dependent entries, and sign-out clears everything. Manual control: base.cache.invalidate({ table }) and base.cache.clear(). React hooks (useQuery, useMutation) live in @potalab/base/react.

Idempotent requests (GET and HEAD, including rpc with get or head) are retried up to 2 times on 429, honoring Retry-After. Writes are never retried: the 429 comes back as error.code = "rate_limited" with error.retry_after in seconds.