npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pocketbase-db-collection

v0.2.0

Published

Pocketbase collection adapter for TanStack DB

Readme

pocketbase-db-collection

A PocketBase collection adapter for TanStack DB. It lets you use a PocketBase RecordService as a real-time, local-first data source for a TanStack DB collection.

Features

  • Real-time sync via PocketBase's subscribe(); realtime events are applied as upserts, so echoes of your own writes never raise duplicate-key errors
  • Initial data fetch with getFullList() after subscribing (no missed events), or query-driven loading with syncMode: 'on-demand'
  • Optimistic mutations forwarded to PocketBase (create, update, delete); the server response is written back into the synced state as soon as the mutation settles, so rows never flicker while waiting for the realtime echo
  • Realtime events and mutation responses are batched into one commit per macrotask (tunable with batchDelay), so a burst of server writes does not freeze the page with one live-query recompute per record
  • Client-generated ids (collection.utils.newId()) so the optimistic row and the server row share the same key
  • Optional transform to shape every record coming from PocketBase (dates, computed fields)
  • Optional Standard Schema for typed and validated mutations
  • Sync failures (expired token, network) surface through the collection error state
  • Automatic unsubscribe on collection cleanup, collection.utils.refetch() to resync from the server

Installation

npm install pocketbase-db-collection @tanstack/db pocketbase
# or
bun add pocketbase-db-collection @tanstack/db pocketbase

Peer dependencies

  • @tanstack/db >=0.9.0 <1 (sync failures are reported through markError, which 0.9 introduced)
  • pocketbase >=0.26.0 <1 (tested against 0.28)
  • Node.js >=19 for the CJS build: collection.utils.newId() relies on the global crypto

Usage

Basic

import { createCollection } from '@tanstack/db'
import PocketBase from 'pocketbase'
import { pocketbaseCollectionOptions } from 'pocketbase-db-collection'

type Todo = {
  id: string
  title: string
  done: boolean
}

const pb = new PocketBase('http://localhost:8090')

const todos = createCollection(
  pocketbaseCollectionOptions({
    recordService: pb.collection<Todo>('todos'),
  }),
)

// Wait for the initial sync to complete
await todos.stateWhenReady()

// Read
const all = todos.toArray
const one = todos.get('record-id')

// Mutate: generate the id on the client so the optimistic row keeps its key
todos.insert({ id: todos.utils.newId(), title: 'Buy milk', done: false })
todos.update('record-id', (draft) => {
  draft.done = true
})
todos.delete('record-id')

PocketBase accepts client-provided ids (15 lowercase alphanumeric characters), which is exactly what newId() produces. Inserting without an id still works, but TanStack DB then keeps a temporary optimistic row next to the server row until the next sync change, so always pass one.

Passing PocketBase options

The options field is forwarded to getFullList(), getList() and subscribe(). Use it for filters, expand, fields, sort.

const todos = createCollection(
  pocketbaseCollectionOptions({
    recordService: pb.collection<Todo>('todos'),
    options: {
      filter: 'done = false',
      expand: 'author',
      sort: '-created',
    },
  }),
)

Transforming records

transform runs on every record coming from PocketBase: initial fetch, realtime events and mutation responses. Use it to parse dates or derive fields.

const appointments = createCollection(
  pocketbaseCollectionOptions({
    recordService: pb.collection('appointments'),
    transform: (record) => ({ ...record, datetime: new Date(record.datetime) }),
  }),
)

On-demand loading

With syncMode: 'on-demand' the collection subscribes to realtime events but does not fetch everything up front. Live queries drive the loading: their where, orderBy and limit clauses are compiled into a PocketBase filter, sort and page size, combined with the base options.filter.

import { createCollection, eq, gte } from '@tanstack/db'
import { useLiveQuery } from '@tanstack/react-db'

const appointments = createCollection(
  pocketbaseCollectionOptions({
    recordService: pb.collection('appointments'),
    syncMode: 'on-demand',
    options: { expand: 'customer,service' },
  }),
)

// Loads `status = 'CONFIRMED' && datetime >= '2026-09-01 00:00:00.000Z'` sorted by `-datetime`
useLiveQuery((q) =>
  q
    .from({ appointment: appointments })
    .where(({ appointment }) => eq(appointment.status, 'CONFIRMED'))
    .where(({ appointment }) => gte(appointment.datetime, new Date('2026-09-01')))
    .orderBy(({ appointment }) => appointment.datetime, 'desc'),
)

Supported operators: eq, gt, gte, lt, lte, like, ilike, in, and, or, and not on eq and in (a negated like cannot be expressed with PocketBase's !~ contains operator). Field references may be nested (calendar.organization). Expressions that cannot be translated fall back to the base filter, which loads a superset that the live query then filters locally. perPage is only sent when the PocketBase filter is exact: like (case-sensitive in TanStack DB, not in PocketBase) and ilike without a % wildcard (PocketBase turns it into a contains match) load the full superset instead. Values are bound with pb.filter() when the record service exposes its client.

With a Standard Schema

Any Standard Schema validator (Zod, Valibot, ArkType, …) can be passed via schema for typed records and validated mutations. The schema validates what you insert; use transform for what comes from the server.

import { z } from 'zod'

const todoSchema = z.object({
  id: z.string(),
  title: z.string(),
  done: z.boolean(),
})

const todos = createCollection(
  pocketbaseCollectionOptions({
    recordService: pb.collection('todos'),
    schema: todoSchema,
  }),
)

Sessions

A collection is bound to the PocketBase auth state it was created with. When the user signs out or switches account, clean the collection up and let it restart with the new session:

pb.authStore.onChange(() => {
  todos.cleanup()
})

await todos.utils.refetch() replaces the synced state with a fresh getFullList() without restarting the subscription. TanStack DB shares utils between every collection created from the same options object, so all of them are refetched.

API

pocketbaseCollectionOptions(config)

Returns a CollectionConfig for TanStack DB's createCollection().

| Field | Type | Description | | --- | --- | --- | | recordService | RecordService<TItem> | A PocketBase record service (pb.collection('...')). Required. | | options | RecordFullListOptions | Optional. Forwarded to getFullList(), getList() and subscribe(). | | transform | (record: RecordModel) => TItem | Optional. Applied to every record coming from PocketBase. | | batchDelay | number | Optional, default 0. Milliseconds to buffer realtime events and mutation responses before committing them together. Raise it to fold longer bursts into fewer commits. | | schema | StandardSchemaV1 | Optional. Validates mutations and types records. | | syncMode | 'eager' \| 'on-demand' | Optional, TanStack DB option. on-demand skips the initial fetch and loads from live queries. | | Other | — | Any other BaseCollectionConfig field (id, gcTime, startSync, autoIndex, compare, …) is forwarded as-is. |

The returned config sets getKey to the record id, registers the sync function (subscribe, then initial fetch or loadSubset), the onInsert / onUpdate / onDelete handlers, and utils:

| Util | Description | | --- | --- | | newId() | A PocketBase-compatible record id. | | refetch() | Replaces the synced state with the current server state. |

Filter helpers

compileWhere, compileSort, combineFilters and buildSubsetRequest are exported for adapters that need to translate TanStack DB expressions to PocketBase filters themselves.

Development

Requires Bun >= 1.2.

bun install
bun test            # unit tests with coverage
bun run type-check
bun run lint
bun run build       # ESM, CJS and declarations in dist/

License

MIT