Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 10 additions & 18 deletions apps/docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,28 @@ export default defineConfig({
plugins: [
lucode({
navLinks: [
{ label: "Docs", link: "/getting-started/installation/" },
{ label: "API", link: "/api/programmatic/" },
{ label: "Docs", link: "/tutorials/your-first-sdk/" },
{ label: "Reference", link: "/reference/cli/" },
],
}),
],
sidebar: [
{ label: "Home", link: "/" },
{
label: "Getting Started",
autogenerate: { directory: "getting-started" },
label: "Tutorials",
autogenerate: { directory: "tutorials" },
},
{
label: "Configuration",
autogenerate: { directory: "configuration" },
label: "How-to Guides",
autogenerate: { directory: "how-to" },
},
{
label: "Generated Output",
autogenerate: { directory: "generated-output" },
label: "Reference",
autogenerate: { directory: "reference" },
},
{
label: "CLI",
autogenerate: { directory: "cli" },
},
{
label: "Plugins",
autogenerate: { directory: "plugins" },
},
{
label: "API",
autogenerate: { directory: "api" },
label: "Explanation",
autogenerate: { directory: "explanation" },
},
],
editLink: {
Expand Down
54 changes: 54 additions & 0 deletions apps/docs/src/content/docs/explanation/generated-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: The generated files
description: What types.gen.ts, client.gen.ts, and sdk.gen.ts each do, and why pbkit splits them.
sidebar:
order: 2
---

A default run writes three files into your `output` directory. They are split by
responsibility, and knowing which is which tells you what to import and what to
leave alone.

| File | Responsibility | You import from it |
|---|---|---|
| `types.gen.ts` | Pure TypeScript types — no runtime code | Yes, for type annotations |
| `client.gen.ts` | A single configured PocketBase client instance | Rarely — directly only for advanced cases |
| `sdk.gen.ts` | Typed CRUD functions that use the client | Yes, for every data call |

## Why three files instead of one

The split mirrors the boundary between **types** and **runtime code**.

`types.gen.ts` contains only `type`/`interface` declarations, so it is erased at
build time and can be imported with `import type` from anywhere — including
environments where you would never want a PocketBase client (shared packages,
edge configs, test fixtures).

`client.gen.ts` is the one place a concrete client is instantiated, using your
`sdk.baseUrl`. Isolating it means there is exactly one client to configure or
replace, and the type-only file stays free of runtime imports.

`sdk.gen.ts` is the runtime surface you actually call. It depends on both of the
other files: it uses the types for its signatures and the client to make
requests. Each function also accepts a per-call `client` override, so you are
never locked into the singleton — see
[Generated SDK](/reference/generated-sdk#crud-functions).

Plugins add their own `*.gen.ts` files (such as `tanstack.gen.ts` or
`zod.gen.ts`) alongside these, following the same convention.

## Treat them as build artifacts

The `output` directory is **cleared and rewritten on every run**. That has two
consequences:

- **Never edit a `.gen.ts` file by hand** — your changes will be lost on the next
generate. Put custom logic in your own modules that import from the generated SDK.
- **Re-generate whenever the schema changes.** The files are a snapshot of the
schema at generation time; the [`--watch`](/reference/cli#watch-mode) flag keeps
them current during development.

Whether you commit the generated files or generate them in CI is your choice.
Committing them makes diffs reviewable and builds reproducible without a live
PocketBase; generating in CI keeps them guaranteed-fresh. Either works because
generation is deterministic for a given schema.
66 changes: 66 additions & 0 deletions apps/docs/src/content/docs/explanation/how-pbkit-works.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: How pbkit works
description: The pipeline pbkit runs to turn a PocketBase schema into typed code, and why it is shaped that way.
sidebar:
order: 1
---

pbkit is a code generator. Every run follows the same three-stage pipeline:
**parse → represent → generate**. Understanding these stages explains most of
pbkit's behaviour and configuration.

## The pipeline

```
schema source ──► parse ──► Schema IR ──► generate ──► .gen.ts files
(URL / JSON / (normalized (types, client,
SQLite) collections sdk, + plugins)
+ relations)
```

### 1. Parse

pbkit reads your schema from whatever source you configure in `input` — a live
PocketBase API, an exported JSON file, or (programmatically) a SQLite database.
Each source has a different raw shape, so each has its own parser
(`parseApi`, `parseJson`, `parseSqlite`).

### 2. The Schema IR

All parsers produce the same output: a **Schema Intermediate Representation
(IR)**. This is a normalized, source-agnostic description of your collections,
their fields, and the relations between them.

The IR is the heart of pbkit. Because every generator and every plugin consumes
the IR rather than raw PocketBase data, they don't care where the schema came
from — generating from a live API and from an exported JSON file produce
identical output. The IR is also what plugins receive as `ctx.ir`, which is why
a plugin can work without knowing anything about API tokens or file paths.

### 3. Generate

The generators walk the IR and emit code:

- the **types** generator produces `types.gen.ts`
- the **SDK** generator produces `client.gen.ts` and `sdk.gen.ts`
- each **plugin** produces its own files (e.g. `tanstack.gen.ts`, `zod.gen.ts`)

Finally, pbkit clears the `output` directory and writes all files. The directory
is rewritten on every run — see [The generated files](/explanation/generated-files)
for why that is safe and how you should treat the output.

## Why a generator instead of a runtime library?

pbkit could have been a runtime library that infers types from your schema at
runtime. It is a generator instead because generated code is **plain, readable
TypeScript you can open and inspect**, it has **zero runtime cost** beyond the
official PocketBase SDK, and your editor gets **full autocomplete** with no
type-level gymnastics. The trade-off is that generated code can drift from your
schema — which is why you re-run `pbkit generate` whenever the schema changes
(or use [`--watch`](/reference/cli#watch-mode)).

## Where this shows up

- The `input` options map directly to the parse stage — see [Configuration](/reference/configuration#input).
- `types.*` and `sdk.*` options tune the generate stage.
- Plugins hook into the generate stage with access to the IR — see [Write a plugin](/how-to/write-a-plugin).
55 changes: 55 additions & 0 deletions apps/docs/src/content/docs/explanation/relations-and-expand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: Relations and expand paths
description: How pbkit models relations as a graph and derives typed expand paths from it.
sidebar:
order: 3
---

PocketBase lets you fetch related records in a single request through the
`expand` query parameter. pbkit turns this into a typed experience by computing,
ahead of time, every expand path a collection can legally use. This page
explains how those paths are derived; for the resulting types and how to use
them, see [Expand types](/reference/expand-types).

## Relations form a graph

When pbkit builds the [Schema IR](/explanation/how-pbkit-works#2-the-schema-ir),
it records each relation field as an edge between two collections. The result is
a directed graph: collections are nodes, relation fields are edges.

For example:

- `articles` → `author` (to `users`) and `categories` (to `categories`)
- `comments` → `article` (to `articles`) and `author` (to `users`)

## Expand paths are walks through the graph

A valid expand path is simply a walk along these edges starting from a
collection. From `comments` you can expand `article`, and from there `article`'s
own relations — `article.author`, `article.categories` — and so on.

pbkit enumerates these walks up to a maximum length and emits them as a union:

```ts
export type CommentsExpand = "article" | "article.author" | "article.categories" | "author"
```

This is why expand autocomplete only ever offers paths that actually exist in
your schema — they are computed from the graph, not guessed.

## Why depth is bounded

The graph can be deep, and following every walk to its end would produce huge,
mostly-useless unions. So traversal stops at `types.expandDepth` levels
(default: `2`). Lower it to `1` for direct relations only; raise it if you
routinely expand deeply nested relations. This is a deliberate trade-off between
type completeness and the size and noise of the generated unions.

## Why cycles don't break generation

Relations frequently form cycles — `users` may reference `articles` which
reference `users` again. A naive walk would recurse forever. pbkit detects when
a walk revisits a collection it is already inside and stops there, so a cyclic
schema still produces a finite set of paths. The depth bound is the second
safeguard: even without an explicit cycle, traversal can never exceed
`expandDepth`.
71 changes: 71 additions & 0 deletions apps/docs/src/content/docs/explanation/why-a-generated-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Why a generated SDK
description: The reasoning behind generating CRUD functions instead of typing the PocketBase client.
sidebar:
order: 4
---

The most common way to get types onto PocketBase is to generate type
declarations and cast the client — `pb.collection("articles")` returns a typed
service. pbkit takes a different approach: it generates **standalone functions**
like `getArticle()` and `listArticles()`. This page explains why.

## The two models

With a typed-client tool, you keep PocketBase's method API and bolt types on top:

```ts
const pb = new PocketBase(url) as TypedPocketBase
const article = await pb.collection("articles").getOne("id")
```

With pbkit, the collection name and method are baked into a named function:

```ts
const article = await getArticle("id")
```

(See [Migrating from pocketbase-typegen](/how-to/migrate-from-pocketbase-typegen)
for a full mapping between the two.)

## What generating functions buys you

**Discoverability.** Typing `getA…` surfaces `getArticle`, `getFirstArticle` and
friends through normal autocomplete. There is no string collection name to
remember or mistype — a wrong name is a missing import, caught immediately.

**Tighter types per operation.** A generated `createArticle` takes
`ArticlesCreate`, while `updateArticle` takes `ArticlesUpdate`. A single typed
`collection()` service tends to share one record type across create, update, and
read, which is looser than what each operation actually accepts. pbkit splits
these because PocketBase treats them differently — see
[Generated types](/reference/generated-types).

**Typed expand without generics.** Because each function knows its collection,
its `expand` option is typed to that collection's
[expand paths](/explanation/relations-and-expand) automatically. The typed-client
model usually requires you to pass the expanded shape as a generic by hand.

**A place to add capabilities.** Generating the call site lets pbkit thread
extra options through every operation — a per-call `client` override and a custom
`fetch` for SSR frameworks — uniformly. These live in `sdk.gen.ts` rather than
being patched onto the PocketBase client.

## The trade-offs

This approach is not free:

- **More generated code.** A function per operation per collection is more output
than a single set of type declarations. pbkit keeps it readable and lets you
[disable operations or whole collections](/how-to/configure-collections) to
trim it.
- **A generation step in the loop.** You re-run `pbkit generate` when the schema
changes. This is the same trade-off any generator makes — discussed in
[How pbkit works](/explanation/how-pbkit-works#why-a-generator-instead-of-a-runtime-library).
- **Less direct.** You call generated wrappers, not the raw SDK. When you need the
underlying client, it is still there in `client.gen.ts`, and any function
accepts a `client` override.

If you only want types and prefer to keep calling `pb.collection(...)`, you can
set [`sdk.enabled: false`](/reference/generated-sdk#disable-sdk-generation) and
use the generated types directly.
63 changes: 0 additions & 63 deletions apps/docs/src/content/docs/generated-output/expand-types.md

This file was deleted.

Loading
Loading