Collections
Packages that store data declare a types.ts (the TypeScript schema), a collections.ts (the pbtsdb registration), and a pb-migrations/ directory (the PocketBase migrations). The generator merges your schema type into core’s MergedSchema so every collection is fully typed end-to-end.
Declaring collections in the manifest
collections: {
register: 'collections',
types: 'types',
},
migrations: { directory: 'pb-migrations' },
Both register and types are subpaths (no extension). The example above resolves to ./collections.ts and ./types.ts inside the package.
types.ts
Define one interface per collection and export a schema type that maps collection names to record types and relations:
import type { Orgs, Users } from '~/types/pbSchema'
export interface Example {
id: string
title: string
org: string
created_by: string
created: string
updated: string
}
export type ExampleSchema = {
example: {
type: Example
relations: {
org: Orgs
created_by: Users
}
}
}
The schema type name follows the convention {PascalSlug}Schema: slug example becomes ExampleSchema, slug task-lists becomes TaskListsSchema. The generator imports this name by convention, so deviating breaks wiring.
Each record interface must match the PocketBase collection schema declared in your migration. Keep the two in lock-step - the typecheck does not cross the boundary into PocketBase’s runtime schema.
collections.ts
Export a registerCollections function. It receives a typed newCollection factory (already parameterized by the merged schema) and core’s CoreStores so you can reference core collections in expand configs without circular imports:
import type { createCollection } from 'pbtsdb/core'
import { BasicIndex } from 'pbtsdb/core'
import type { Schema } from '~/types/pbSchema'
import type { CoreStores } from '@tinycld/core/lib/pocketbase'
import type { ExampleSchema } from './types'
type MergedSchema = Schema & ExampleSchema
export function registerCollections(
newCollection: ReturnType<typeof createCollection<MergedSchema>>,
coreStores: CoreStores,
) {
const example = newCollection('example', {
omitOnInsert: ['created', 'updated'] as const,
expand: { org: coreStores.orgs },
collectionOptions: {
autoIndex: 'eager' as const,
defaultIndexType: BasicIndex,
},
})
return { example }
}
The keys of the returned object become the names you pass to useStore() in components. Returning { example } makes useStore('example') work everywhere.
Reading from a package collection
Inside a screen or hook, import useStore and useOrgLiveQuery from core:
import { useStore } from '@tinycld/core/lib/pocketbase'
import { useOrgLiveQuery } from '@tinycld/core/lib/pocketbase'
import { eq } from '@tanstack/db'
export default function ExampleList() {
const [exampleCollection] = useStore('example')
const { data } = useOrgLiveQuery((query, { orgId }) =>
query
.from({ example: exampleCollection })
.where(({ example }) => eq(example.org, orgId))
.orderBy(({ example }) => example.title, 'asc'),
)
// render data
}
useOrgLiveQuery auto-scopes to the active org and waits for org context to load, which is what you want for nearly every package query. Reach for raw useLiveQuery only in bootstrap hooks or for genuinely user-level data.
Migrations
Place PocketBase migration files in the directory you named in manifest.migrations.directory. Use timestamp-prefixed filenames and follow PocketBase’s JS migration format:
/// <reference path="../../../server/pb_data/types.d.ts" />
migrate(
(app) => {
const collection = new Collection({
id: 'example',
name: 'example',
type: 'base',
fields: [
// field definitions
],
})
return app.save(collection)
},
(app) => {
const collection = app.findCollectionByNameOrId('example')
return app.delete(collection)
},
)
The generator symlinks each migration into server/pb_migrations/ so PocketBase applies it on next boot. Prefix your collection names with the package slug (example_items, mail_threads) to avoid cross-package collisions in the shared PocketBase database.