Query data

Every read in a package goes through pbtsdb and TanStack DB. You grab a collection handle with useStore, describe what you want with TanStack DB operators, and wrap the whole thing in useOrgLiveQuery so the query waits for the signed-in user and can scope rows to them.

Before you query

Your package must declare its collections (see Collections). Once the generator has wired them into MergedSchema, the name you return from registerCollections is the name you pass to useStore.

The pattern

import { useStore } from '@tinycld/core/lib/pocketbase'
import { useOrgLiveQuery } from '@tinycld/core/lib/use-org-live-query'
import { eq } from '@tanstack/db'

export default function ExampleList() {
    const [exampleCollection] = useStore('example')
    const { data } = useOrgLiveQuery((query, { userId }) =>
        query
            .from({ example: exampleCollection })
            .where(({ example }) => eq(example.owner, userId))
            .orderBy(({ example }) => example.title, 'asc')
    )
    return <ItemList items={data ?? []} />
}

useStore accepts variadic collection names and returns a tuple. Destructure it positionally:

const [tagsCollection] = useStore('tags')
const [jobsCollection, addressesCollection] = useStore('jobs', 'addresses')

The query DSL

TanStack DB’s builder mirrors SQL. All operators are imported from @tanstack/db:

import { and, eq, gt, inArray, like, or } from '@tanstack/db'

query
    .from({ item: itemsCollection })
    .join(
        { user: usersCollection },
        ({ item, user }) => eq(item.owner, user.id),
        'left'
    )
    .where(({ item }) =>
        and(
            eq(item.owner, userId),
            or(eq(item.status, 'active'), gt(item.updated, lastWeek))
        )
    )
    .orderBy(({ item }) => item.updated, 'desc')
    .select(({ item, user }) => ({ ...item, ownerName: user.name }))

Use .select() when you want to compute a derived shape - it runs in the reactive pipeline, so downstream re-renders only happen when the computed value changes.

Scoping to the user

A TinyCld deployment is one workspace, so there is no organization to scope by. The scope object useOrgLiveQuery hands your callback is { userId } — the signed-in user’s id — and you use it to filter “my own rows”: owner, author, and user foreign keys on package collections point straight at users. Shared data (a calendar’s events, a board’s cards) is filtered by the record’s own relations instead, and PocketBase’s collection rules decide what the user is allowed to see in the first place.

The only exceptions are the bootstrap hooks that user scoping itself depends on — @tinycld/core’s use-current-role — and genuinely session-level queries. Packages should use useOrgLiveQuery everywhere.

Where queries live

Prefer inline queries in the screen or component that uses them. A hook per query makes the data flow invisible to future readers and encourages accidental duplication. Extract a shared hook only when the exact same query is called in three or more places - until then, the inline form is the honest one.

// good - data flow is visible at the point of use
export default function ExampleList() {
    const [exampleCollection] = useStore('example')
    const { data } = useOrgLiveQuery((query, { userId }) =>
        query.from({ example: exampleCollection }).where(({ example }) => eq(example.owner, userId))
    )
    return <ItemList items={data ?? []} />
}

The eq operator (and and, or, gt, etc.) is re-exported from @tinycld/core/lib/pocketbase for convenience; you can also import it directly from @tanstack/db. Either works — pick one per file and stay consistent.

Common mistakes

For writes, see Mutate data.