# PocketBase — AI Agent Guide PocketBase is an open-source backend with an embedded SQLite database, realtime subscriptions, built-in auth, file storage, an admin dashboard, and a simple REST API. It ships as a single ~12 MB binary (Linux, macOS, Windows) with zero dependencies. The JS SDK (`pocketbase` on npm) is the primary client. This guide is organized for AI agents. Scan **Part 1** to find the recipe that matches your task, follow the recipe in **Part 2**, and consult **Part 3** for lookup tables and schemas. --- # PART 1 · AGENT INDEX ## How to use this guide 1. Find your task in the **Intent Index** below. Follow the recipe ID (e.g. `R-AUTH-01`). 2. If you hit an error, check the **Symptom Index**. 3. Every recipe is self-contained: `Use when` / `Prereqs` / code / `Pitfalls`. Cross-refs (`→ R-XXX-NN`) point to related recipes. 4. The **Reference** in Part 3 holds full tables (field types, filter operators, collection JSON schema, admin endpoints). Recipes link to it instead of duplicating. 5. Read **Critical Rules** below once at session start — they apply to everything. ## Intent Index — "I need to..." | Goal | Recipe | |---|---| | Run a PocketBase server (local or cloud) | R-SETUP-01 | | Install the JS SDK in my app | R-SETUP-02 | | Get a superuser token for admin/agent tasks | R-SETUP-03 | | Define a data model (collections, fields) | R-DATA-01 | | Import a collection schema programmatically | R-DATA-02 | | Import collections that reference each other | R-DATA-03 | | Add password login | R-AUTH-01 | | Add OAuth2 (Google, GitHub, …) | R-AUTH-02 | | Add OTP or MFA | R-AUTH-03 | | Check auth state, refresh, log out | R-AUTH-04 | | Auth on the server (cookies, SSR) | R-AUTH-05 | | Email verification + password reset | R-AUTH-06 | | Impersonate a user / manage linked OAuth | R-AUTH-07 | | Authenticate as superuser via SDK | R-AUTH-08 | | List, paginate, sort, full-list records | R-CRUD-01 | | Get / create / update / delete a record | R-CRUD-02 | | Filter records (with user input) | R-CRUD-03 | | Fetch related records (`expand`) | R-CRUD-04 | | Batch many writes in one request | R-CRUD-05 | | Handle errors and auto-cancellation | R-CRUD-06 | | Subscribe to live updates | R-RT-01 | | Realtime in Node.js / SSR | R-RT-02 | | Upload files (single or multiple) | R-FILE-01 | | Replace or delete files | R-FILE-02 | | Build file URLs, thumbnails, protected files, S3 | R-FILE-03 | | Lock down a collection (API rules) | R-RULES-01 | | Run server code on record events | R-HOOK-01 | | Add a custom HTTP route | R-HOOK-02 | | Query the DB from a hook | R-HOOK-03 | | Share code between hooks | R-HOOK-04 | | Integrate with React | R-CLIENT-REACT | | Integrate with Vue | R-CLIENT-VUE | | Integrate with Svelte (SSR) | R-CLIENT-SVELTE | | Integrate with Astro (SSR) | R-CLIENT-ASTRO | | Integrate with Nuxt 3 | R-CLIENT-NUXT | | Use the SDK on Node/Deno/Bun | R-CLIENT-NODE | | Use the SDK in a static `.html` page | R-CLIENT-VANILLA | | Deploy to PocketBaseCloud | R-DEPLOY-01 | | Self-host + production checklist | R-DEPLOY-02 | | Manage collections via REST | R-ADMIN-01 | | Read/update app settings via REST | R-ADMIN-02 | | Read server logs via REST | R-ADMIN-03 | | List or trigger cron jobs | R-ADMIN-04 | | Create, restore, download backups | R-ADMIN-05 | | Use TypeScript with the SDK | R-MISC-01 | | Intercept SDK requests (`beforeSend`/`afterSend`) | R-MISC-02 | | Use multiple auth collections (multi-tenant) | R-MISC-03 | ## Symptom Index — "I'm seeing..." | Symptom | See | |---|---| | `401` on admin REST call | R-SETUP-03 (token expired — get a fresh one) | | `403` / Cloudflare `1010` from Python `urllib` on PB Cloud | R-DATA-04 (use `curl` or `requests`) | | `"The relation collection doesn't exist."` on import | R-DATA-03 (import parents first; resolve `collectionId`) | | `EventSource is not defined` (Node) | R-RT-02 (register polyfill) | | Filter contains user input — injection risk | R-CRUD-03 (use `pb.filter()`) | | `400` with `err.response.data..code: validation_*` | R-CRUD-06 | | Hook never fires | R-HOOK-01 (`e.next()`, collection arg, file in `pb_hooks/`) | | `setTimeout` doesn't work in a hook | R-HOOK-04 (no async in hooks) | | First auth returns `401` with `{mfaId}` | R-AUTH-03 (complete second factor) | | MFA enabled and first call to `authWithPassword` fails | R-AUTH-03 | | File 403 on URL fetch | R-FILE-03 (protected files need a file token) | ## Critical Rules — read once, apply always 1. **Never ask the user for their PocketBase password.** Use the token-bridge flow (R-SETUP-03). Never store or log a user password. 2. **Always use `pb.filter()`** for any filter expression that contains user-provided input — prevents injection (R-CRUD-03). 3. **Import collections in dependency order.** Resolve `collectionId` on relation fields to the actual `pbc_…` ID *before* importing the child collection (R-DATA-03). 4. **Pin SDK versions** (`pocketbase@0.26.8`) — never use `@latest` in production HTML. 5. **`deleteMissing: true` on `/api/collections/import` is destructive** — it drops every collection not in the payload. Default to `false`. 6. **Hooks are synchronous.** No `setTimeout`, `setInterval`, or `async/await` constructs that defer past `e.next()` — handler runs to completion or the request hangs. 7. **Superuser tokens bypass all API rules.** Treat them like root credentials and rotate often (default expiry: 1 day). 8. **Auto-cancellation:** the SDK cancels duplicate in-flight requests by URL. Pass `{ requestKey: null }` if two genuinely-parallel requests look identical (R-CRUD-06). 9. **JSON fields** are the only nullable field type; every other field type defaults to its zero value (`""`, `0`, `false`, `[]`, `{lon:0,lat:0}`). 10. **`/api/files/...` is the file path; the SDK helper `pb.files.getURL()` is preferred** — it handles record-vs-string args correctly. --- # PART 2 · RECIPES ## Setup ### R-SETUP-01 — Run a PocketBase server **Use when:** you need an instance to develop against or deploy to. **Related:** R-DEPLOY-01 (cloud), R-DEPLOY-02 (self-host). **Option A — PocketBaseCloud (recommended, no install):** Sign up at [pocketbasecloud.com](https://pocketbasecloud.com) with Google. The free plan gives 1 instance, 50 MB storage, SSL, and frontend hosting. The API URL is ready immediately. **Option B — Local binary:** ```bash # Download the single binary for your platform from pocketbase.io (~12 MB) ./pocketbase serve --http=0.0.0.0:8090 # Create a superuser (first time only) ./pocketbase superuser create admin@example.com password123 ``` Result: - REST API at `http://127.0.0.1:8090/api/` - Admin dashboard at `http://127.0.0.1:8090/_/` - Data lives in `pb_data/` (SQLite + uploaded files) - Server-side hooks go in `pb_hooks/` (create the directory next to the binary; see R-HOOK-01) The admin dashboard is where you create collections, set API rules, manage users, import/export schema JSON, and configure auth providers. **Pitfalls** - The binary has zero dependencies — no Docker, no separate DB to install. - `pb_data/` contains the database; back it up before destructive admin actions. --- ### R-SETUP-02 — Install the JS SDK **Use when:** building an app that talks to PocketBase. **With a bundler (npm):** ```bash npm install pocketbase ``` ```js import PocketBase from 'pocketbase'; const pb = new PocketBase('https://your-instance.pocketbasecloud.com'); ``` **Without a bundler (CDN, static HTML):** ```html ``` **Pitfalls** - **Pin a version** (`@0.26.8`) rather than `@latest`; the SDK occasionally has breaking changes. - Node < 17 needs a `fetch` polyfill (`import 'cross-fetch/polyfill'`). - For realtime in Node, also register an EventSource polyfill (R-RT-02). --- ### R-SETUP-03 — Get a superuser token for agent/admin tasks **Use when:** you need to call admin-only endpoints (create/import collections, change settings, run backups). **Never ask the user for their password.** **The flow:** 1. Ask the user to open **[https://pocketbasecloud.com/token-bridge.html](https://pocketbasecloud.com/token-bridge.html)**. 2. They enter their instance URL, superuser email, and password — auth happens entirely in their browser; credentials never leave their machine. 3. They click **Copy with Prompt** (or **Copy Token**) and paste the result back to you. 4. You now have a superuser JWT. **Using the token:** ```bash # Authorization header is the bare token (no "Bearer " prefix) curl -s "https://YOUR_INSTANCE/api/collections" \ -H "Authorization: $TOKEN" ``` **Pitfalls** - Tokens expire in **1 day** by default. Configurable in admin dashboard → `_superusers` → Token duration. - A `401` on a previously-working call almost always means the token expired — direct the user back to the token-bridge. - Never store, log, or echo the password — only the token. - For non-cloud instances or programmatic auth, see R-AUTH-08. --- ## Data Model ### R-DATA-01 — Define collections and fields **Use when:** designing your data model. **Reference:** Field Types table → §Ref-Fields. Collection JSON schema → §Ref-CollectionJSON. **Collection types:** | Type | Purpose | |---|---| | `base` | General data (posts, products, todos). | | `auth` | Like `base` + built-in `email`, `password`, `verified`, `tokenKey` fields. Multiple auth collections are supported (R-MISC-03). | | `view` | Read-only. Data from a SQL `SELECT`. Useful for aggregations. | **Field types** (summary; full options in §Ref-Fields): | Field | JS value | Notable options | |---|---|---| | `text` | `""` | `min`, `max`, `pattern`, `autogeneratePattern` | | `number` | `0` | `min`, `max`, `onlyInt` | | `bool` | `false` | — | | `email` | `""` | `exceptDomains`, `onlyDomains` | | `url` | `""` | `onlyDomains`, `exceptDomains` | | `editor` | `""` | (HTML stored as string) | | `date` | `""` | RFC 3339 `min`/`max` | | `autodate` | auto | `onCreate`, `onUpdate` | | `select` | `""` or `[]` | `values` (required), `maxSelect` | | `file` | `""` or `[]` | `maxSelect`, `maxSize`, `mimeTypes`, `protected`, `thumbs` | | `relation` | `""` or `[]` | `collectionId` (required), `cascadeDelete`, `maxSelect`, `minSelect` | | `json` | `null` | the **only** nullable field type | | `geopoint` | `{lon,lat}` | default `{0,0}` | **Create via Admin UI:** 1. Open `https://your-instance/_/`. 2. Click **Collections → New Collection**. 3. Choose type (`base` / `auth` / `view`), add fields, save. **Pitfalls** - All non-`json` fields are non-nullable (zero-default). - Auth collections have a special **Manage** rule that lets one user manage another's data (R-RULES-01). - For a UNIQUE constraint, add a UNIQUE index in the field's options. --- ### R-DATA-02 — Import collections from JSON **Use when:** scripting schema setup, copying schema between instances, or syncing dev → prod. **Via Admin UI:** Settings → Import Collections, paste JSON, confirm. **Via SDK (superuser only):** ```js await pb.collections.import([collectionJSON], /* deleteMissing */ false); ``` **Via REST (superuser token — see R-SETUP-03):** ```bash curl -s -X PUT "$INSTANCE/api/collections/import" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"collections": [...], "deleteMissing": false}' ``` **Pitfalls** - `deleteMissing: true` **drops every collection** not in the payload. Always pass `false` unless you genuinely want destruction. - For collections that reference others, see R-DATA-03 (must resolve `collectionId` first). - When passing JSON via shell, write to a temp file (`curl -d @file.json`) instead of inlining — special chars in rules (`!`, `'`, `"`, `=`) break inline escaping. See R-DATA-04. - Full schema reference: §Ref-CollectionJSON. --- ### R-DATA-03 — Import collections with relations (dependency order) **Use when:** importing two or more collections where one has a `relation` field pointing to another. The referenced collection must exist *before* the relation field is validated. If both are new in the same import, you get `"The relation collection doesn't exist."`. ```bash # Step 1: Import the parent (referenced) collection first curl -s -X PUT "$INSTANCE/api/collections/import" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"collections": [], "deleteMissing": false}' # Step 2: Get the parent's assigned ID curl -s "$INSTANCE/api/collections" -H "Authorization: $TOKEN" \ | python3 -c "import sys,json; [print(c['name'],c['id']) for c in json.load(sys.stdin)['items']]" # Step 3: Patch the child's relation field with the real "pbc_..." ID, then import curl -s -X PUT "$INSTANCE/api/collections/import" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"collections": [], "deleteMissing": false}' ``` **Pitfalls** - Using `"collectionId": "vouchers"` (name) only works if the target collection already exists. New-in-same-batch references fail. - Always resolve to the literal `pbc_…` ID before importing. --- ### R-DATA-04 — Troubleshoot HTTP issues against PocketBaseCloud **curl vs Python `urllib`:** PocketBaseCloud's WAF may block `urllib.request` (returns `403` with code `1010`) while `curl` works. - **Prefer `curl`** for REST calls — works consistently. - Or use the PocketBase JS SDK. - If you must use Python, try the `requests` library with a standard `User-Agent` header. **Shell escaping for API rules:** rules contain `!`, `'`, `"`, `=` — inlining JSON is error-prone. ```bash # Write the JSON to a file, then use -d @file python3 -c "import json; json.dump({...}, open('/tmp/payload.json','w'))" curl -s -X PUT "$INSTANCE/api/collections/import" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d @/tmp/payload.json ``` --- ## Authentication PocketBase uses **stateless JWT** auth. Clients send `Authorization: TOKEN`. There is no logout endpoint — discard the token locally. ### R-AUTH-01 — Password sign-up + login **Use when:** basic email/password auth in a client app. **Prereqs:** an auth collection exists (default: `users`); SDK initialized as `pb` (R-SETUP-02). **Related:** R-AUTH-04 (token mgmt), R-AUTH-05 (SSR), R-AUTH-06 (verification). ```js // Sign up const user = await pb.collection('users').create({ email: 'test@example.com', password: '1234567890', passwordConfirm: '1234567890', name: 'Jane', }); // Log in const authData = await pb.collection('users').authWithPassword( 'test@example.com', '1234567890', ); console.log(authData.token); // JWT console.log(authData.record); // user record // pb.authStore.{isValid, token, record} are now populated. ``` **Pitfalls** - `passwordConfirm` is required on create. - If MFA is on, the first call returns `401` with `{mfaId}` — see R-AUTH-03. - The SDK auto-stores the token in `pb.authStore`; manual handling only needed for SSR (R-AUTH-05). --- ### R-AUTH-02 — OAuth2 login (Google, GitHub, …) **Use when:** adding social login. Configure the provider first in admin dashboard → users collection → OAuth2. **All-in-one (recommended for browser apps):** ```js const authData = await pb.collection('users').authWithOAuth2({ provider: 'google', createData: { name: 'New User' }, // optional, populates new accounts }); ``` **Manual code exchange** (when you control the OAuth callback URL): ```js // Step 1: get auth URL const authUrl = await pb.collection('users').authWithOAuth2Code( 'google', 'https://yourapp.com/oauth-callback' ); // Step 2: redirect user to authUrl // Step 3: on the callback page, exchange code + verifier from URL params const authData = await pb.collection('users').authWithOAuth2Code( 'google', code, codeVerifier, 'https://yourapp.com/oauth-callback' ); ``` **Pitfalls** - The all-in-one form opens a popup — won't work in non-browser environments. Use the manual flow for those. - See R-AUTH-07 to list or unlink OAuth providers on a user. --- ### R-AUTH-03 — OTP and MFA **One-Time Password (OTP)** — emails a code to the user, then exchanges it for a token: ```js // Step 1: request OTP (sent to user's email) const result = await pb.collection('users').requestOTP('test@example.com'); // Step 2: auth with the code the user typed const authData = await pb.collection('users').authWithOTP(result.otpId, '123456'); ``` **Multi-Factor Auth (MFA)** — when enabled on the collection, the first method returns `401` with `{mfaId}`; pass it to the second method: ```js // First factor (password) succeeds → 401 with mfaId // Second factor: await pb.collection('users').authWithPassword('test@example.com', '1234567890', { mfaId: '...', }); ``` **Pitfalls** - OTP/MFA must be enabled on the collection (admin dashboard → users → Options). - The `mfaId` is short-lived; complete the second factor promptly. --- ### R-AUTH-04 — Manage tokens (authStore) **Use when:** checking auth state, refreshing tokens, listening for changes, or logging out. ```js pb.authStore.isValid; // boolean — has a non-expired token pb.authStore.record; // user record or null pb.authStore.token; // JWT string pb.authStore.isSuperuser; // true if authenticated as superuser // Refresh (e.g. on page reload to extend the session) await pb.collection('users').authRefresh(); // React to auth changes pb.authStore.onChange((token, record) => { /* update UI */ }); // Log out (there is no server endpoint — just clear local state) pb.authStore.clear(); // Programmatically set state (e.g. after parsing a cookie) pb.authStore.save(token, record); ``` **Pitfalls** - `pb.authStore.isValid` checks expiry locally — it does not contact the server. - For SSR you usually want a custom store (R-AUTH-05). --- ### R-AUTH-05 — Server-side auth (cookies, async store) **Use when:** SSR frameworks (SvelteKit, Astro, Nuxt, Next, Express) or non-browser runtimes (Node, Deno, Bun). **Related:** R-CLIENT-SVELTE, R-CLIENT-ASTRO, R-CLIENT-NUXT, R-CLIENT-NODE. **Cookie-based pattern (per-request):** ```js // Parse auth from request cookie (default key: 'pb_auth') pb.authStore.loadFromCookie(request.headers.get('cookie') || ''); // Export back to a Set-Cookie header on the response response.headers.set('set-cookie', pb.authStore.exportToCookie({ secure: true, sameSite: 'lax', httpOnly: true, })); ``` **Async store (persistent auth in non-cookie environments):** ```js import PocketBase, { AsyncAuthStore } from 'pocketbase'; const store = new AsyncAuthStore({ save: async (serialized) => localStorage.setItem('pb_auth', serialized), initial: localStorage.getItem('pb_auth'), }); // Substitute a file/memory store for Node/Deno/Bun const pb = new PocketBase('https://example.com', store); ``` **Pitfalls** - Create a **fresh `PocketBase` instance per request** on the server — sharing one across requests leaks auth between users. - `loadFromCookie` reads the raw `Cookie` header value, not a parsed object. --- ### R-AUTH-06 — Email verification, password reset, email change ```js // Email verification await pb.collection('users').requestVerification('test@example.com'); await pb.collection('users').confirmVerification('TOKEN_FROM_EMAIL'); // Password reset await pb.collection('users').requestPasswordReset('test@example.com'); await pb.collection('users').confirmPasswordReset( 'TOKEN_FROM_EMAIL', 'newPassword', 'newPassword', ); // Email change (requires the user to be currently authenticated) await pb.collection('users').requestEmailChange('new@example.com'); await pb.collection('users').confirmEmailChange('TOKEN_FROM_EMAIL', 'userPassword'); ``` **Pitfalls** - SMTP must be configured in admin dashboard → Settings → Mail settings; otherwise no email goes out. - Email templates and token durations are per-auth-collection — see §Ref-CollectionJSON (auth section). --- ### R-AUTH-07 — External auths & impersonation ```js // List linked OAuth2 providers for a record const providers = await pb.collection('users').listExternalAuths('RECORD_ID'); // Unlink an OAuth2 provider await pb.collection('users').unlinkExternalAuth('RECORD_ID', 'google'); // Impersonate a user (superuser only) — returns a new isolated client const impersonated = await pb.collection('users').impersonate('RECORD_ID', 3600); // Use `impersonated` for subsequent calls as that user ``` **Pitfalls** - Impersonation requires superuser auth on the current `pb` instance. - The returned client has its own `authStore`; the original is unchanged. --- ### R-AUTH-08 — Authenticate as superuser via SDK **Use when:** scripting from Node/Deno/Bun. For interactive agent flows, prefer the token-bridge (R-SETUP-03). ```js await pb.collection('_superusers').authWithPassword('admin@example.com', 'password'); // pb.authStore.isSuperuser === true // Superusers bypass all API rules. ``` **Health check** (no auth required): ```js const health = await pb.health.check(); // { code: 200, message: "API is healthy." } ``` --- ## CRUD ### R-CRUD-01 — List records ```js // Paginated const result = await pb.collection('todos').getList(1, 20, { filter: 'completed = false', sort: '-created', expand: 'user', fields: 'id,title,completed,expand.user.name', }); // Get every record (auto-batched) const all = await pb.collection('todos').getFullList({ batch: 1000 }); // First matching record const first = await pb.collection('todos').getFirstListItem('title = "Important"'); ``` **Pitfalls** - `getFullList` paginates internally — fine for thousands, dangerous for millions. - Filter strings with user input must use `pb.filter()` (R-CRUD-03). --- ### R-CRUD-02 — Get / create / update / delete ```js const record = await pb.collection('todos').getOne('RECORD_ID', { expand: 'user' }); const created = await pb.collection('todos').create({ title: 'Buy groceries', completed: false, user: 'USER_RECORD_ID', }); await pb.collection('todos').update('RECORD_ID', { completed: true }); await pb.collection('todos').delete('RECORD_ID'); ``` **Pitfalls** - `create`/`update` accept a plain object **or** `FormData` (used for file uploads — R-FILE-01). - The `+` suffix appends to multi-value fields (`'tags+': ['urgent']`); the `-` suffix removes (R-FILE-02). --- ### R-CRUD-03 — Filter syntax and safe parameter binding **Operators:** `=`, `!=`, `>`, `>=`, `<`, `<=`, `~` (like/contains), `!~`, `?=`, `?!=`, `?>`, `?>=`, `?<`, `?<=`, `?~`, `?!~`. The `?` prefix means "any element matches" for multi-value fields. **Grouping:** `&&` (AND), `||` (OR), `()`. **Always use `pb.filter()` when the filter contains user input** — it escapes values and prevents injection: ```js const filter = pb.filter( 'user = {:userId} && (status = "active" || role ~ {:role})', { userId: 'abc123', role: 'admin' }, ); // pb.filter supports string, number, boolean, Date, null ``` **Common examples:** ``` completed = true created > "2024-01-01 00:00:00" title ~ "important" // LIKE %important% category ?= "work" // any multi-value equals "work" user = @request.auth.id // in API rules: current user @request.auth.id != "" // any authenticated user ``` **Pitfalls** - Naively concatenating user input into a filter string is a security bug. The `~` operator's wildcard chars are part of the user-controlled string. --- ### R-CRUD-04 — Expand relations and select fields ```js // Expand: fetch related records inline (up to 6 levels deep) const todo = await pb.collection('todos').getOne('ID', { expand: 'user,comments.user', }); // todo.expand.user -> the related user record // todo.expand.comments -> array of comments, each with expand.user // Fields selector: limit returned fields (cuts payload size) const records = await pb.collection('todos').getList(1, 20, { fields: 'id,title,completed,expand.user.name', }); // :excerpt modifier truncates text fields // fields: '*,description:excerpt(200,true)' ``` **Pitfalls** - `expand.foo` keys in `fields` must match keys in `expand`. - Expanding deep relations triggers more DB queries — keep it shallow when paginating. --- ### R-CRUD-05 — Batch many writes in one request ```js const batch = pb.createBatch(); batch.collection('todos').create({ title: 'Task 1' }); batch.collection('todos').create({ title: 'Task 2' }); batch.collection('todos').update('RECORD_ID', { completed: true }); batch.collection('todos').delete('OTHER_ID'); batch.collection('todos').upsert({ id: 'X', title: 'Create or update' }); const result = await batch.send(); // result is an array of { status, body } per operation ``` **Pitfalls** - Supports `create`, `update`, `delete`, `upsert`. **No reads** (`getList`, `getOne`) in batch. - Must be enabled in admin dashboard → Settings → Application → Batch API. --- ### R-CRUD-06 — Error handling and auto-cancellation **Error shape:** ```js try { await pb.collection('todos').create({ title: '' }); } catch (err) { err.status; // 400 err.response; // { data: { title: { code: 'validation_required', message: '...' } } } err.url; // the request URL err.isAbort; // true if cancelled (see below) } ``` **Auto-cancellation:** the SDK auto-cancels duplicate in-flight requests keyed by URL: ```js pb.collection('todos').getList(1, 20); // cancelled pb.collection('todos').getList(1, 20); // only this one executes pb.autoCancellation(false); // disable globally pb.collection('todos').getList(1, 20, { requestKey: null }); // never cancel this one pb.cancelAllRequests(); pb.cancelRequest('myKey'); ``` **Pitfalls** - Two legitimately-parallel requests with identical URLs need distinct `requestKey` values, or one will be cancelled. - `err.isAbort` is true on cancellation — don't surface it as a user error. --- ## Realtime ### R-RT-01 — Subscribe to record changes PocketBase uses **Server-Sent Events** for realtime. Subscribe to a whole collection (`*`) or a single record (`RECORD_ID`). Events fire on **create**, **update**, **delete**. ```js const unsubscribe = await pb.collection('todos').subscribe('*', (data) => { data.action; // 'create' | 'update' | 'delete' data.record; // the record }, { expand: 'user', // headers: { 'X-Custom': 'value' }, }); // Single record await pb.collection('todos').subscribe('RECORD_ID', (data) => { if (data.action === 'update') updateUI(data.record); }); // Unsubscribe pb.collection('todos').unsubscribe('RECORD_ID'); // one topic pb.collection('todos').unsubscribe('*'); // all wildcard topics pb.collection('todos').unsubscribe(); // every topic on this collection pb.realtime.isConnected; // boolean ``` **Authorization** (checked at subscription time, not connect time): - `*` (whole collection) → the collection's **ListRule** applies. - `RECORD_ID` (single record) → the collection's **ViewRule** applies. **Pitfalls** - If the user logs out, existing subscriptions are not automatically re-authorized — re-subscribe. - In Node/SSR, you need an EventSource polyfill (R-RT-02). --- ### R-RT-02 — Realtime in Node.js / SSR ```js import { EventSource } from 'eventsource'; global.EventSource = EventSource; // React Native import EventSource from 'react-native-sse'; global.EventSource = EventSource; ``` **Low-level `pb.realtime` API** (custom topics, not record-tied): ```js pb.realtime.subscribe('my-topic', (data) => { /* ... */ }); pb.realtime.unsubscribe('my-topic'); pb.realtime.unsubscribeByPrefix('my-'); pb.realtime.unsubscribeByTopicAndListener('my-topic', myCallback); pb.realtime.isConnected; // Auto-reconnect happens automatically; hook for visibility: pb.realtime.onDisconnect = (activeSubs) => console.log('Disconnected', activeSubs); ``` For record changes, prefer `pb.collection().subscribe()` (R-RT-01) — `pb.realtime` is for app-defined topics. --- ## File Storage Files upload as `multipart/form-data` on `create`/`update`. URL pattern: `/api/files/{collection}/{recordId}/{filename}`. ### R-FILE-01 — Upload one or many files ```js // Single file via plain object await pb.collection('posts').create({ title: 'Hello', image: new File([fileInput.files[0]], 'photo.jpg', { type: 'image/jpeg' }), }); // Single file via FormData const form = new FormData(); form.set('title', 'Hello'); form.set('image', fileInput.files[0]); await pb.collection('posts').create(form); // Multi-file field (maxSelect >= 2): pass an array await pb.collection('posts').create({ title: 'Gallery', photos: [ new File([fileInput.files[0]], 'photo1.jpg', { type: 'image/jpeg' }), new File([fileInput.files[1]], 'photo2.jpg', { type: 'image/jpeg' }), ], }); // Multi-file via FormData — use append() (not set()) for each const form = new FormData(); form.set('title', 'Gallery'); form.append('photos', file1); form.append('photos', file2); await pb.collection('posts').create(form); // Server-side (Node/Deno/Bun) — use Blob with a type: await pb.collection('posts').create({ title: 'Server Upload', photos: [ new Blob([buffer1], { type: 'image/png' }), new Blob([buffer2], { type: 'image/png' }), ], }); // Append to an existing multi-file field (`+` suffix) await pb.collection('posts').update('RECORD_ID', { 'documents+': new File([bytes], 'report.pdf'), }); await pb.collection('posts').update('RECORD_ID', { 'photos+': [new File([blob3], 'photo3.jpg'), new File([blob4], 'photo4.jpg')], }); ``` **Pitfalls** - For multi-file FormData, you **must** call `append()` per file; `set()` overwrites. - Field must be configured as `file` type with sufficient `maxSelect` and `maxSize` in the collection schema. --- ### R-FILE-02 — Replace, clear, or delete specific files ```js // Clear a single-file field await pb.collection('posts').update('RECORD_ID', { image: '' }); // Clear an entire multi-file field await pb.collection('posts').update('RECORD_ID', { photos: [] }); // Remove specific files from a multi-file field (`-` suffix) await pb.collection('posts').update('RECORD_ID', { 'documents-': ['old_report.pdf', 'draft.txt'], }); ``` --- ### R-FILE-03 — File URLs, thumbnails, protected files, S3 **Build a URL:** ```js const url = pb.files.getURL(record, record.image); // -> https://YOUR_INSTANCE/api/files/posts/RECORD_ID/photo_abc123.jpg // Force a download (Content-Disposition: attachment) const dl = pb.files.getURL(record, record.image, { download: 1 }); ``` **Thumbnails** (image fields only; append `?thumb=SIZE`): ``` ?thumb=100x300 # crop center ?thumb=100x300t # crop top ?thumb=100x300b # crop bottom ?thumb=100x300f # fit inside (no crop) ?thumb=0x300 # resize to height ?thumb=100x0 # resize to width ``` Supported: jpg, png, gif (first frame), webp (stored as png). **Protected files** — mark the field as **Protected** in the collection schema; then a short-lived file token is required: ```js const token = await pb.files.getToken(); const url = pb.files.getURL(record, record.secretDoc, { token }); // The token is evaluated against the collection's ViewRule. // Empty ViewRule = anyone with the URL can fetch; no token needed. ``` **S3 storage:** default is local disk (`pb_data/storage`). Switch to S3-compatible (AWS S3, MinIO, Wasabi, DigitalOcean Spaces, Cloudflare R2) in admin → Settings → Files storage. Test the connection via `POST /api/settings/test/s3` (R-ADMIN-02). **Pitfalls** - File tokens last only ~2 minutes by default — refetch for each request. - For an unprotected file, omit the `token` option. --- ## Access Control ### R-RULES-01 — Define API access rules Each collection has 5 rules. Each is a filter expression. **Empty string = no restriction (public). `null` = superuser-only.** | Rule | Controls | |---|---| | `listRule` | Who can list/search records | | `viewRule` | Who can view a single record | | `createRule` | Who can create records | | `updateRule` | Who can update records | | `deleteRule` | Who can delete records | Auth collections also have a **manage** rule (allows user A to manage user B's record). Superusers always bypass every rule. **Special variables in rules** (full table in §Ref-RuleVars): | Variable | Value | |---|---| | `@request.auth.id` | ID of authenticated user (empty if anonymous) | | `@request.auth.email` | Email of authenticated user | | `@request.auth.role` | Value of a `role` field on the user record | | `@request.method` | HTTP method | | `@request.data.*` | Submitted form data (during create/update) | | `@collection.*` | Cross-collection lookup | **Common patterns:** ``` # Any authenticated user @request.auth.id != "" # Owner-only (record has a 'user' relation field pointing to users) user = @request.auth.id # Role-based (user record has a 'role' field) @request.auth.role = "admin" # Mixed: owner OR admin user = @request.auth.id || @request.auth.role = "admin" # Typical pattern: public read, auth-only writes, owner-only edits listRule: "" viewRule: "" createRule: "@request.auth.id != ''" updateRule: "user = @request.auth.id" deleteRule: "user = @request.auth.id || @request.auth.role = 'admin'" ``` **Pitfalls** - The same expression that's safe in a *rule* is unsafe in a *user query* — always use `pb.filter()` for user input (R-CRUD-03). - An empty string is **public**; `null` is **superuser-only**. They are not the same. --- ## Server Hooks Hooks are server-side JavaScript that runs inside the PocketBase process. Put `*.pb.js` files in `pb_hooks/` next to the binary; changes auto-reload (UNIX only). ### R-HOOK-01 — Set up hooks and write event handlers **File layout:** ``` ./pocketbase ./pb_data/ ./pb_hooks/ main.pb.js # any *.pb.js file is loaded utils.js # plain .js modules for require() ``` **Type hints:** include `/// ` at the top of each hook file. **Basic shape:** ```js // pb_hooks/main.pb.js /// onRecordAfterCreateSuccess((e) => { console.log('New record created:', e.record.get('title')); e.next(); }, 'todos'); onRecordUpdateRequest((e) => { if (!e.record.getString('title')) { throw new BadRequestError('Title is required'); } e.next(); }, 'todos'); ``` **Common event hooks** (full list in §Ref-HookEvents): ```js onBootstrap(e => { e.next(); }); onTerminate(e => { e.next(); }); onRecordCreateRequest(e => { e.next(); }, 'todos'); onRecordAfterCreateSuccess(e => { e.next(); }, 'todos'); onRecordUpdateRequest(e => { e.next(); }, 'todos'); onRecordAfterUpdateSuccess(e => { e.next(); }, 'todos'); onRecordDeleteRequest(e => { e.next(); }, 'todos'); onRecordAfterDeleteSuccess(e => { e.next(); }, 'todos'); onRecordAuthRequest(e => { e.next(); }, 'users'); onRecordAuthRefreshRequest(e => { e.next(); }, 'users'); onMailerSend(e => { e.next(); }); ``` **Pitfalls** - **Always call `e.next()`** — forgetting it stalls the request. - The second arg is a collection name; omit it for app-wide hooks. - File execution order = filename sort order. --- ### R-HOOK-02 — Add a custom HTTP route ```js routerAdd('GET', '/hello/{name}', (e) => { const name = e.request.pathValue('name'); return e.json(200, { message: `Hello ${name}` }); }); // Protected route — $apis.requireAuth() middleware populates e.auth routerAdd('POST', '/api/custom/stats', (e) => { const authRecord = e.auth; // guaranteed non-null const todos = $app.findRecordsByFilter( 'todos', 'user = {:uid}', undefined, 10, 0, { uid: authRecord.id } ); return e.json(200, { count: todos.length, items: todos }); }, $apis.requireAuth()); ``` **Pitfalls** - Path segments use `{name}` syntax; access via `e.request.pathValue('name')`. - Without `$apis.requireAuth()` middleware, `e.auth` may be `null`. --- ### R-HOOK-03 — Query the database from a hook ```js // Find by ID const r = $app.findRecordById('todos', 'RECORD_ID'); // Find by filter (use param binding, just like client-side) const records = $app.findRecordsByFilter( 'todos', 'user = {:userId} && completed = false', '-created', // sort 20, // perPage 0, // page { userId: 'abc123' }, ); // Create const newRecord = new Record(); newRecord.set('title', 'From hook'); $app.save(newRecord); // Update const r = $app.findRecordById('todos', 'ID'); r.set('completed', true); $app.save(r); // Delete $app.delete(r); // Raw SQL (with bound params) const rows = $app.db() .newQuery('SELECT id, title FROM todos WHERE user = {:uid}') .bind({ uid: 'abc123' }) .all(); ``` **Pitfalls** - `json` field values need `.get()` / `.set()` — they're not auto-converted. - Raw SQL bypasses API rules but respects DB constraints. --- ### R-HOOK-04 — Share code between hooks; caveats **Hooks run in isolated contexts.** Share code via `require()` against `__hooks`: ```js // pb_hooks/utils.js module.exports = { hello: name => `Hello ${name}`, sendWelcomeEmail: (email) => { /* ... */ }, }; // pb_hooks/main.pb.js onRecordAfterCreateSuccess((e) => { e.next(); const utils = require(`${__hooks}/utils.js`); utils.sendWelcomeEmail(e.record.getString('email')); }, 'users'); ``` **Global objects available in hooks:** | Object | Purpose | |---|---| | `$app` | PocketBase app instance — DB access, record ops | | `$apis` | API routing helpers, middlewares | | `$os` | OS operations (file system, shell) | | `$security` | JWT, AES encryption, random strings | | `__hooks` | Absolute path to `pb_hooks/` | **Caveats** - **No `setTimeout` / `setInterval`** — handlers must run synchronously. - `json` field values: `.get()` / `.set()`. - **CommonJS only** natively — ESM needs precompilation. - File execution order follows filename sort order. --- ## Client Integration Each recipe assumes a single shared client at module scope and basic familiarity with its framework. Reuse the patterns in R-AUTH-* and R-CRUD-* inside the framework's lifecycle hooks. ### R-CLIENT-REACT ```jsx import { useEffect, useState } from 'react'; import PocketBase from 'pocketbase'; const pb = new PocketBase('https://your-instance.pocketbasecloud.com'); function App() { const [todos, setTodos] = useState([]); useEffect(() => { pb.collection('todos').getFullList({ sort: '-created' }).then(setTodos); }, []); const addTodo = async (title) => { const record = await pb.collection('todos').create({ title, completed: false }); setTodos(prev => [record, ...prev]); }; return
    {todos.map(t =>
  • {t.title}
  • )}
; } ``` --- ### R-CLIENT-VUE ```js import { createApp, ref, onMounted } from 'vue'; import PocketBase from 'pocketbase'; createApp({ setup() { const pb = new PocketBase('https://your-instance.pocketbasecloud.com'); const todos = ref([]); onMounted(async () => { todos.value = await pb.collection('todos').getFullList({ sort: '-created' }); }); const addTodo = async (title) => { const r = await pb.collection('todos').create({ title, completed: false }); todos.value.unshift(r); }; return { todos, addTodo }; }, }).mount('#app'); ``` --- ### R-CLIENT-SVELTE Server hook that loads/saves auth cookie per request (R-AUTH-05): ```js // src/hooks.server.js import PocketBase from 'pocketbase'; export async function handle({ event, resolve }) { event.locals.pb = new PocketBase('https://your-instance.pocketbasecloud.com'); event.locals.pb.authStore.loadFromCookie(event.request.headers.get('cookie') || ''); const response = await resolve(event); response.headers.set('set-cookie', event.locals.pb.authStore.exportToCookie()); return response; } ``` --- ### R-CLIENT-ASTRO ```js // src/middleware.ts import PocketBase from 'pocketbase'; export const onRequest = async (context, next) => { context.locals.pb = new PocketBase('https://your-instance.pocketbasecloud.com'); context.locals.pb.authStore.loadFromCookie(context.request.headers.get('cookie') || ''); const response = await next(); response.headers.set('set-cookie', context.locals.pb.authStore.exportToCookie()); return response; }; ``` --- ### R-CLIENT-NUXT ```js // plugins/pocketbase.js export default defineNuxtPlugin(() => { const pb = new PocketBase('https://your-instance.pocketbasecloud.com'); const cookie = useCookie('pb_auth', { path: '/', secure: true, sameSite: 'strict' }); pb.authStore.save(cookie.value?.token, cookie.value?.record); pb.authStore.onChange(() => { cookie.value = { token: pb.authStore.token, record: pb.authStore.record }; }); return { provide: { pb } }; }); // In components: const { $pb } = useNuxtApp(); ``` --- ### R-CLIENT-NODE ```js import PocketBase from 'pocketbase'; // Node < 17: import 'cross-fetch/polyfill'; // For realtime, also register an EventSource polyfill (R-RT-02). // Create a FRESH instance per request to avoid auth bleed const pb = new PocketBase('https://your-instance.pocketbasecloud.com'); // Superuser auth for admin work await pb.collection('_superusers').authWithPassword('admin@example.com', 'password'); // Or auth as a specific user await pb.collection('users').authWithPassword('user@example.com', 'password'); const records = await pb.collection('todos').getFullList(); ``` --- ### R-CLIENT-VANILLA ```html ``` Works in any static `.html` page — no `npm`, no bundler. Pin the version (R-SETUP-02). --- ## Deployment ### R-DEPLOY-01 — Deploy to PocketBaseCloud (managed) **Plans:** | Plan | Price | What you get | |---|---|---| | **Free** | $0/mo | 1 PB instance, 50 MB storage, frontend hosting, SSL, shared server, community support (30-day inactivity cleanup) | | **Starter** | $5/mo | 1 PB instance, 3 GB storage, separate server cluster, custom domain, 6 global locations, email + Discord support | | **Pro** | from $13/mo | Unlimited PB instances, dedicated server, custom storage/bandwidth, Node.js/Deno/Bun backend hosting, multi-project, live monitoring, priority support | All plans include 99.9% uptime SLA, SSL, DDoS protection, firewall, and automated backups. Regions: Germany (×2), Finland, US East, US West, Singapore. **Steps:** 1. Go to [pocketbasecloud.com](https://pocketbasecloud.com), sign up with Google. 2. Pick a plan → instance is ready in ~30 seconds. 3. Export your dev collections (admin → Settings → Export Collections). 4. Import into the new instance (admin → Settings → Import Collections), or replicate manually. 5. Point your app at the provided URL. --- ### R-DEPLOY-02 — Self-host + production checklist **Self-hosting docs:** [pocketbase.io/docs/going-to-production/](https://pocketbase.io/docs/going-to-production/). **Production checklist:** - Enable the auto-backup cron in admin → Settings → Backups (`{{baseUrl}}/_/#/settings/backups`). Configure S3 storage for backups (Cloudflare R2 is free up to 10 GB). - Set **strict API rules** on every collection (R-RULES-01). Empty rule = public. - Move file storage to S3-compatible (R-FILE-03) — local disk is fine for dev, not for HA. - Rotate superuser tokens; default lifetime is 1 day (R-SETUP-03). - Confirm SMTP works (R-ADMIN-02 → test email) so password resets actually send. --- ## Admin REST API Every admin endpoint needs a superuser token (R-SETUP-03). Header: `Authorization: $TOKEN` (no `Bearer` prefix). ### R-ADMIN-01 — Manage collections via REST **Endpoints** (also in §Ref-AdminEndpoints): | Action | Method | Path | |---|---|---| | List collections | `GET` | `/api/collections` | | Get collection | `GET` | `/api/collections/{idOrName}` | | Create collection | `POST` | `/api/collections` | | Update collection | `PATCH` | `/api/collections/{idOrName}` | | Delete collection | `DELETE` | `/api/collections/{idOrName}` | | Truncate collection | `DELETE` | `/api/collections/{idOrName}/truncate` | | Import collections | `PUT` | `/api/collections/import` | | Get scaffolds | `GET` | `/api/collections/meta/scaffolds` | List query params: `page` (default 1), `perPage` (default 30), `sort`, `filter`, `fields`, `skipTotal`. ```bash # List all curl -s "$INSTANCE/api/collections" -H "Authorization: $TOKEN" # Get one curl -s "$INSTANCE/api/collections/{collectionName}" -H "Authorization: $TOKEN" # Create a base collection curl -s -X POST "$INSTANCE/api/collections" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"notes","type":"base","fields":[{"name":"title","type":"text","required":true},{"name":"body","type":"editor"}]}' # Update (PATCH = partial) curl -s -X PATCH "$INSTANCE/api/collections/{collectionIdOrName}" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"renamed","listRule":"","viewRule":""}' # Delete (returns 204) curl -s -o /dev/null -w "%{http_code}" -X DELETE \ "$INSTANCE/api/collections/{collectionIdOrName}" \ -H "Authorization: $TOKEN" # Truncate — delete all records, keep schema (returns 204) curl -s -o /dev/null -w "%{http_code}" -X DELETE \ "$INSTANCE/api/collections/{collectionIdOrName}/truncate" \ -H "Authorization: $TOKEN" # Import from a JSON file curl -s -X PUT "$INSTANCE/api/collections/import" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d "$(python3 -c "import json; data=json.load(open('schema.json')); print(json.dumps({'collections': data, 'deleteMissing': False}))")" # Scaffold templates curl -s "$INSTANCE/api/collections/meta/scaffolds" -H "Authorization: $TOKEN" ``` **Use-case examples:** ```bash # Enable Google OAuth on the users collection curl -X PATCH "$INSTANCE/api/collections/_pb_users_auth_" \ -H "Authorization: $TOKEN" \ --data-raw '{"id":"_pb_users_auth_","name":"users","oauth2":{"providers":[{"name":"google","clientId":"...","clientSecret":"..."}],"mappedFields":{"id":"","name":"name","username":"","avatarURL":"avatar"},"enabled":true}}' ``` **All-in-one script — authenticate then import schema:** ```bash #!/bin/bash # Usage: ./import-schema.sh schema.json admin@email.com password INSTANCE="https://YOUR_INSTANCE" TOKEN=$(curl -s -X POST "$INSTANCE/api/collections/_superusers/auth-with-password" \ -H "Content-Type: application/json" \ -d "{\"identity\":\"$2\",\"password\":\"$3\"}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") curl -s -X PUT "$INSTANCE/api/collections/import" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d "$(python3 -c "import json; data=json.load(open('$1')); print(json.dumps({'collections': data, 'deleteMissing': False}))")" echo "Done." ``` **Pitfalls** - Body schema is in §Ref-CollectionJSON. `name` is required on create; everything else is optional. - `deleteMissing: true` drops collections not in the payload — never use on production unless you're rebuilding from scratch. --- ### R-ADMIN-02 — Read or update app settings | Action | Method | Path | |---|---|---| | Get all settings | `GET` | `/api/settings` | | Update settings | `PATCH` | `/api/settings` | | Test S3 connection | `POST` | `/api/settings/test/s3` | | Test email | `POST` | `/api/settings/test/email` | | Generate Apple client secret | `POST` | `/api/settings/apple/generate-client-secret` | Secrets are redacted as `******` in GET responses. ```bash # Get all curl -s "$INSTANCE/api/settings" -H "Authorization: $TOKEN" # Update (PATCH is partial — only send what changes) curl -s -X PATCH "$INSTANCE/api/settings" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"meta":{"appName":"My App","appUrl":"https://example.com"}}' # Test S3 connection curl -s -X POST "$INSTANCE/api/settings/test/s3" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"filesystem":"storage"}' # "storage" or "backups" # Test email delivery curl -s -X POST "$INSTANCE/api/settings/test/email" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"collection":"_superusers","email":"test@example.com","template":"verification"}' # template: "verification" | "password-reset" | "email-change" # Apple Sign-In client secret curl -s -X POST "$INSTANCE/api/settings/apple/generate-client-secret" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"clientId":"com.example.app","teamId":"ABC123DEFG","keyId":"ABC123DEFG","privateKey":"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----","duration":2592000}' ``` Settings body keys (all optional on PATCH): `meta`, `logs`, `backups`, `smtp`, `s3`, `batch`, `rateLimits`, `trustedProxy`. Full schema in §Ref-SettingsBody. --- ### R-ADMIN-03 — Read server logs | Action | Method | Path | |---|---|---| | List logs | `GET` | `/api/logs` | | Get one log | `GET` | `/api/logs/{id}` | | Aggregated stats | `GET` | `/api/logs/stats` | List params: `page`, `perPage`, `sort`, `filter`, `fields`. Filterable: `id`, `created`, `updated`, `level`, `message`, `data.*`. Levels: `-4` DEBUG, `0` INFO, `4` WARN, `8` ERROR. ```bash # Recent logs curl -s "$INSTANCE/api/logs?perPage=50&sort=-created" -H "Authorization: $TOKEN" # Errors only curl -s "$INSTANCE/api/logs?filter=level>=8&sort=-created" -H "Authorization: $TOKEN" # By URL pattern curl -s 'https://YOUR_INSTANCE/api/logs?filter=(data.url~'\''test.com'\''&&level>0)' -H "Authorization: $TOKEN" # Single entry curl -s "$INSTANCE/api/logs/{id}" -H "Authorization: $TOKEN" # Stats (grouped by date+hour) curl -s "$INSTANCE/api/logs/stats" -H "Authorization: $TOKEN" # Response: [{"total":4,"date":"2022-06-01 19:00:00.000Z"}, ...] ``` --- ### R-ADMIN-04 — List or run cron jobs | Action | Method | Path | |---|---|---| | List crons | `GET` | `/api/crons` | | Run a cron now | `POST` | `/api/crons/{jobId}` | ```bash curl -s "$INSTANCE/api/crons" -H "Authorization: $TOKEN" # Built-ins: # __pbDBOptimize__ "0 0 * * *" # __pbMFACleanup__ "0 * * * *" # __pbOTPCleanup__ "0 * * * *" # __pbLogsCleanup__ "0 */6 * * *" # Trigger now (returns 204) curl -s -X POST "$INSTANCE/api/crons/__pbLogsCleanup__" -H "Authorization: $TOKEN" ``` --- ### R-ADMIN-05 — Backups (create, upload, restore, download) | Action | Method | Path | |---|---|---| | List backups | `GET` | `/api/backups` | | Create backup | `POST` | `/api/backups` | | Upload backup | `POST` | `/api/backups/upload` | | Delete backup | `DELETE` | `/api/backups/{key}` | | Restore backup | `POST` | `/api/backups/{key}/restore` | | Download backup | `GET` | `/api/backups/{key}?token=FILE_TOKEN` | Only one backup/restore can run at a time (`400` if one is in progress). Downloads need a file token. ```bash # List curl -s "$INSTANCE/api/backups" -H "Authorization: $TOKEN" # [{"key":"pb_backup_20230519162514.zip","modified":"...","size":251316185}, ...] # Create (204). `name` optional; must match [a-z0-9_-]; auto-generated if omitted. curl -s -X POST "$INSTANCE/api/backups" \ -H "Authorization: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"my_backup"}' # Upload an existing zip (multipart only) curl -s -X POST "$INSTANCE/api/backups/upload" \ -H "Authorization: $TOKEN" \ -F "file=@backup.zip" # Delete (204) curl -s -X DELETE "$INSTANCE/api/backups/pb_backup_20230519162514.zip" \ -H "Authorization: $TOKEN" # Restore — RESTARTS the server (204) curl -s -X POST "$INSTANCE/api/backups/pb_backup_20230519162514.zip/restore" \ -H "Authorization: $TOKEN" # Download (needs a file token from pb.files.getToken() or auth flow) curl -s "$INSTANCE/api/backups/pb_backup_20230519162514.zip?token=FILE_TOKEN" -o backup.zip ``` **Pitfalls** - `/restore` triggers a server restart — clients see a connection drop briefly. - `/api/health` is the only admin-area endpoint that needs no auth — handy for liveness checks. --- ## Misc ### R-MISC-01 — TypeScript with the SDK **Per-call generics:** ```ts interface Task { id: string; name: string; completed: boolean; } const tasks = await pb.collection('tasks').getList(1, 20); // tasks.items is Task[] const task = await pb.collection('tasks').getOne('RECORD_ID'); ``` **Global typed client** (one declaration, fully inferred everywhere): ```ts import PocketBase, { RecordService } from 'pocketbase'; interface Task { id: string; title: string; completed: boolean; } interface Post { id: string; title: string; content: string; } interface TypedPocketBase extends PocketBase { collection(idOrName: string): RecordService; collection(idOrName: 'tasks'): RecordService; collection(idOrName: 'posts'): RecordService; } const pb = new PocketBase('https://your-instance.pocketbasecloud.com') as TypedPocketBase; const task = await pb.collection('tasks').getOne('RECORD_ID'); // Promise const posts = await pb.collection('posts').getList(1, 20); // Promise> ``` --- ### R-MISC-02 — `beforeSend` / `afterSend` hooks (client-side) Modify every outgoing request or incoming response from a single place: ```js pb.beforeSend = function (url, options) { options.headers = Object.assign({}, options.headers, { 'X-Custom-Header': 'example', }); return { url, options }; }; pb.afterSend = function (response, data) { console.log(response.status); return Object.assign(data, { additionalField: 123 }); }; ``` Use cases: injecting auth on every call, logging, response normalization. **Note:** these are client-only hooks (different from server hooks in R-HOOK-*). --- ### R-MISC-03 — Multiple auth collections (multi-tenant) You can have separate auth collections (e.g. `users`, `admins`, `clients`), each with its own auth endpoints at `/api/collections/{collection}/auth-*`. Lets you separate roles or tenants without a single monolithic user table. ```js // Sign a customer in await pb.collection('clients').authWithPassword(email, password); // Or an internal admin (separate from _superusers) await pb.collection('admins').authWithPassword(email, password); ``` **Pitfalls** - Each collection has independent rules, OAuth configs, MFA settings. - The `_superusers` collection is special — its tokens bypass all rules. --- # PART 3 · REFERENCE ## §Ref-Fields — Field types (full) | Field | JS value | Options / attributes | |---|---|---| | `text` | `""` | `min`, `max`, `pattern`, `autogeneratePattern` (used for primary key IDs) | | `number` | `0` | `min`, `max`, `onlyInt` (integer-only if `true`) | | `bool` | `false` | — | | `email` | `""` | `exceptDomains` (blocked), `onlyDomains` (allowed) | | `url` | `""` | `onlyDomains` (allowed), `exceptDomains` (blocked) | | `editor` | `""` | HTML content stored as string; `convertURLs` | | `date` | `""` | `min`, `max` (RFC 3339 datetime bounds) | | `autodate` | auto | `onCreate` (set on insert), `onUpdate` (set on every update) | | `select` | `""` or `[]` | `values` (required), `maxSelect` (1 = single, ≥2 = multi) | | `file` | `""` or `[]` | `maxSelect`, `maxSize` (bytes), `mimeTypes`, `protected`, `thumbs` | | `relation` | `""` or `[]` | `collectionId` (required), `cascadeDelete`, `maxSelect`, `minSelect` | | `json` | `null` | **the only nullable field type** | | `geopoint` | `{lon,lat}` | default `{"lon":0,"lat":0}` | Non-`json` fields are non-nullable with zero-defaults. ## §Ref-FilterOps — Filter operators `=`, `!=`, `>`, `>=`, `<`, `<=`, `~` (LIKE), `!~` (NOT LIKE), `?=`, `?!=`, `?>`, `?>=`, `?<`, `?<=`, `?~`, `?!~`. Prefix `?` means "any element matches" for multi-value (`select` multi, `relation` multi, etc.). Grouping: `&&` (AND), `||` (OR), `()`. Param binding via `pb.filter()` (R-CRUD-03) — supports `string`, `number`, `boolean`, `Date`, `null`. ## §Ref-RuleVars — Special variables in API rules | Variable | Value | |---|---| | `@request.auth.id` | ID of the authenticated user (empty if anonymous) | | `@request.auth.email` | Email of the authenticated user | | `@request.auth.role` | Value of a `role` field on the user record | | `@request.auth.` | Any field on the user record | | `@request.method` | HTTP method (`GET`, `POST`, …) | | `@request.data.*` | Submitted form data during create/update | | `@collection..*` | Cross-collection lookup | Empty rule string = **public**. `null` rule = **superuser-only**. ## §Ref-CollectionJSON — Collection JSON schema (full) **Body shape** (create/update; all top-level keys except `name` optional on create; all optional on update): ``` { name (required on create): string — unique, used as table name type: "base" | "auth" | "view" fields: Array<{ name: string, type: text|number|bool|email|url|editor|date|autodate|select|file|relation|json|geoPoint, required: boolean, primaryKey: boolean, system: boolean, hidden: boolean, // type-specific options: min, max, pattern, autogeneratePattern, onlyInt, // values, maxSelect, minSelect, collectionId, cascadeDelete, maxSize, mimeTypes, // protected, thumbs, exceptDomains, onlyDomains, onCreate, onUpdate, convertURLs }> indexes: Array // SQL CREATE INDEX statements (not for "view") system: boolean // prevents rename/delete of API rules listRule, viewRule, createRule, updateRule, deleteRule: null | string // view-only viewQuery: string // SQL SELECT // auth-only manageRule, authRule: null | string authAlert: { enabled, emailTemplate: { subject, body } } oauth2: { enabled, mappedFields: {id,name,username,avatarURL}, providers: [...] } passwordAuth: { enabled, identityFields: Array } mfa: { enabled, duration, rule } otp: { enabled, duration, length, emailTemplate: { subject, body } } authToken, passwordResetToken, emailChangeToken, verificationToken, fileToken: { duration, secret } verificationTemplate, resetPasswordTemplate, confirmEmailChangeTemplate: { subject, body } } ``` **Example — full collection definition with every common field type:** ```json [ { "id": "pbc_1543120290", "listRule": "@request.auth.id != ''", "viewRule": "@request.auth.id != ''", "createRule": "@request.auth.id != ''", "updateRule": "relationField = @request.auth.id", "deleteRule": "relationField = @request.auth.id", "name": "testCollection", "type": "base", "fields": [ { "type": "text", "id": "text3208210256", "name": "id", "autogeneratePattern": "[a-z0-9]{15}", "max": 15, "min": 15, "pattern": "^[a-z0-9]+$", "primaryKey": true, "required": true, "system": true, "presentable": false, "hidden": false }, { "type": "text", "id": "text1542800728", "name": "field", "max": 200000, "min": 1000, "pattern": "^[a-z0-9]+$", "autogeneratePattern": "", "primaryKey": false, "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "editor", "id": "editor768641678", "name": "editorField", "convertURLs": false, "maxSize": 6000000, "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "number", "id": "number2245382642", "name": "numberField", "max": 100, "min": 20, "onlyInt": true, "required": true, "system": false, "presentable": false, "hidden": false }, { "type": "bool", "id": "bool3280560631", "name": "boolField", "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "email", "id": "email1478965954", "name": "emailField", "exceptDomains": ["temp.com","mailinator.com"], "onlyDomains": [], "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "url", "id": "url2037873857", "name": "urlField", "exceptDomains": [], "onlyDomains": ["github.com","google.com"], "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "date", "id": "date2270773164", "name": "datetimeField", "min": "2026-05-22 12:00:00.000Z", "max": "2029-05-31 12:00:00.000Z", "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "select", "id": "select1865290411", "name": "selectField", "maxSelect": 2, "values": ["option1","option2"], "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "file", "id": "file816577617", "name": "fileFieldSingle", "maxSelect": 1, "maxSize": 7000000, "mimeTypes": ["image/x-xpixmap","application/x-7z-compressed","application/zip","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], "protected": false, "thumbs": [], "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "file", "id": "file2307418135", "name": "multipleFilesField", "maxSelect": 10, "maxSize": 4000000, "mimeTypes": ["image/jpeg","image/png","image/webp"], "protected": false, "thumbs": [], "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "relation", "id": "relation1882921418", "name": "relationField", "collectionId": "_pb_users_auth_", "cascadeDelete": false, "maxSelect": 1, "minSelect": 0, "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "json", "id": "json512575104", "name": "jsonField", "maxSize": 3000000, "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "geoPoint", "id": "geoPoint3983504865", "name": "geoPointField", "required": false, "system": false, "presentable": false, "hidden": false }, { "type": "autodate", "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "system": false, "presentable": false, "hidden": false }, { "type": "autodate", "id": "autodate3332085495", "name": "updated", "onCreate": true, "onUpdate": true, "system": false, "presentable": false, "hidden": false } ], "indexes": ["CREATE INDEX `idx_c6IIDITnkz` ON `testCollection` (`relationField`)"], "system": false } ] ``` **Example — auth collection update payload** (compact, single-line — useful for diffing): ```json {"id":"_pb_users_auth_","listRule":null,"viewRule":"id = @request.auth.id","createRule":"","updateRule":null,"deleteRule":null,"name":"users","type":"auth","fields":[{"autogeneratePattern":"[a-z0-9]{15}","hidden":false,"id":"text3208210256","max":15,"min":15,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"},{"cost":0,"hidden":true,"id":"password901924565","max":0,"min":8,"name":"password","pattern":"","presentable":false,"required":true,"system":true,"type":"password"},{"autogeneratePattern":"[a-zA-Z0-9]{50}","hidden":true,"id":"text2504183744","max":60,"min":30,"name":"tokenKey","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":true,"type":"text"},{"hidden":false,"id":"email3885137012","name":"email","onlyDomains":null,"exceptDomains":null,"presentable":false,"required":true,"system":true,"type":"email"},{"hidden":false,"id":"bool1547992806","name":"emailVisibility","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool256245529","name":"verified","presentable":false,"required":false,"system":true,"type":"bool"},{"autogeneratePattern":"","hidden":false,"id":"text1579384326","max":255,"min":0,"name":"name","pattern":"","presentable":false,"primaryKey":false,"required":false,"system":false,"type":"text"},{"hidden":false,"id":"file376926767","maxSelect":1,"maxSize":0,"mimeTypes":["image/jpeg","image/png","image/svg+xml","image/gif","image/webp"],"name":"avatar","presentable":false,"protected":false,"required":false,"system":false,"thumbs":null,"type":"file"},{"hidden":false,"id":"autodate2990389176","name":"created","onCreate":true,"onUpdate":false,"presentable":false,"system":false,"type":"autodate"},{"hidden":false,"id":"autodate3332085495","name":"updated","onCreate":true,"onUpdate":true,"presentable":false,"system":false,"type":"autodate"}],"indexes":["CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)","CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''"],"system":false,"authRule":"","manageRule":null,"authAlert":{"enabled":true,"emailTemplate":{"subject":"Login from a new location","body":"

...

"}},"oauth2":{"providers":[{"name":"google","clientId":"ExampleClientID","clientSecret":"ExampleClientSecret"}],"mappedFields":{"id":"","name":"name","username":"","avatarURL":"avatar"},"enabled":true},"passwordAuth":{"enabled":false,"identityFields":["email"]},"mfa":{"enabled":false,"duration":1800,"rule":""},"otp":{"enabled":false,"duration":180,"length":8,"emailTemplate":{"subject":"OTP for {APP_NAME}","body":"

Your OTP: {OTP}

"}},"authToken":{"duration":100000},"passwordResetToken":{"duration":1800},"emailChangeToken":{"duration":1800},"verificationToken":{"duration":259200},"fileToken":{"duration":180},"verificationTemplate":{"subject":"Verify your {APP_NAME} email","body":"

Click {APP_URL}/_/#/auth/confirm-verification/{TOKEN}

"},"resetPasswordTemplate":{"subject":"Reset your {APP_NAME} password","body":"

Click {APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}

"},"confirmEmailChangeTemplate":{"subject":"Confirm your {APP_NAME} new email address","body":"

Click {APP_URL}/_/#/auth/confirm-email-change/{TOKEN}

"}} ``` ## §Ref-SettingsBody — Settings body schema Top-level keys (all optional on PATCH): - `meta`: `appName`, `appUrl`, `senderName`, `senderAddress`, `hideControls` - `logs`: `maxDays`, `minLevel` (`-4`=DEBUG, `0`=INFO, `4`=WARN, `8`=ERROR), `logIP`, `logAuthId` - `backups`: `cron` (cron expression), `cronMaxKeep`, `s3` (nested S3 config) - `smtp`: `enabled`, `host`, `port`, `username`, `password`, `tls`, `authMethod`, `localName` - `s3`: `enabled`, `bucket`, `region`, `endpoint`, `accessKey`, `secret`, `forcePathStyle` - `batch`: `enabled`, `maxRequests`, `timeout`, `maxBodySize` - `rateLimits`: `enabled`, `rules` (array of `{label, maxRequests, duration}`) - `trustedProxy`: `headers` (array), `useLeftmostIP` ## §Ref-HookGlobals — Hook globals | Object | Purpose | |---|---| | `$app` | The PocketBase app — DB access (`findRecordById`, `findRecordsByFilter`, `save`, `delete`, `db().newQuery()`) and record ops | | `$apis` | API routing helpers and middlewares (`$apis.requireAuth()`, `$apis.requireSuperuserAuth()`, etc.) | | `$os` | OS operations — file system, shell commands | | `$security` | JWT signing/parsing, AES encryption, random strings | | `__hooks` | Absolute path to `pb_hooks/` directory | ## §Ref-HookEvents — Common event hooks ```js // App lifecycle onBootstrap(e => { e.next(); }); onTerminate(e => { e.next(); }); // Record lifecycle (collection arg optional — omit for global) onRecordCreateRequest(e => { e.next(); }, 'todos'); onRecordAfterCreateSuccess(e => { e.next(); }, 'todos'); onRecordUpdateRequest(e => { e.next(); }, 'todos'); onRecordAfterUpdateSuccess(e => { e.next(); }, 'todos'); onRecordDeleteRequest(e => { e.next(); }, 'todos'); onRecordAfterDeleteSuccess(e => { e.next(); }, 'todos'); // Auth onRecordAuthRequest(e => { e.next(); }, 'users'); onRecordAuthRefreshRequest(e => { e.next(); }, 'users'); // Mail onMailerSend(e => { e.next(); }); ``` ## §Ref-AdminEndpoints — Admin REST API summary All require superuser auth except `/api/health`. | Domain | Endpoints | |---|---| | Collections | `GET/POST /api/collections`, `GET/PATCH/DELETE /api/collections/{id}`, `DELETE /api/collections/{id}/truncate`, `PUT /api/collections/import`, `GET /api/collections/meta/scaffolds` | | Settings | `GET/PATCH /api/settings`, `POST /api/settings/test/s3`, `POST /api/settings/test/email`, `POST /api/settings/apple/generate-client-secret` | | Logs | `GET /api/logs`, `GET /api/logs/{id}`, `GET /api/logs/stats` | | Crons | `GET /api/crons`, `POST /api/crons/{jobId}` | | Backups | `GET/POST /api/backups`, `POST /api/backups/upload`, `DELETE /api/backups/{key}`, `POST /api/backups/{key}/restore`, `GET /api/backups/{key}?token=...` | | Health | `GET /api/health` (no auth) | ## §Ref-SDKCheatsheet — One-line SDK reminders | Goal | Code | |---|---| | Init client | `const pb = new PocketBase('https://...');` | | Sign up + login | `await pb.collection('users').create({ email, password, passwordConfirm, name });` then `authWithPassword` | | Auth check | `if (pb.authStore.isValid) { ... }` | | Current user | `pb.authStore.record` | | List w/ filter + sort | `pb.collection('todos').getList(1, 20, { filter: '...', sort: '-created' })` | | Get all records | `pb.collection('todos').getFullList()` | | Create with file | `pb.collection('posts').create({ title: 'Hi', image: new File(...) })` | | Realtime subscribe | `pb.collection('todos').subscribe('*', (e) => { ... })` | | Expand relations | `{ expand: 'user,comments.user' }` | | Safe filter params | `pb.filter('title ~ {:t}', { t: userInput })` | | Batch | `const b = pb.createBatch(); b.collection('a').create({...}); b.send()` | | SSR cookie flow | `pb.authStore.loadFromCookie(reqCookie)` then `exportToCookie()` on response | | Deploy | [pocketbasecloud.com](https://pocketbasecloud.com) → sign up → 30 s | ## §Ref-ExternalDocs — Official docs When in doubt, verify against the official PocketBase docs: - [Collections API](https://pocketbase.io/docs/api-collections/) — CRUD, import, scaffolds, filter syntax - [Settings API](https://pocketbase.io/docs/api-settings/) — settings endpoints, S3/email tests, Apple client secret - [Logs API](https://pocketbase.io/docs/api-logs/) — list, view, stats, filter - [Crons API](https://pocketbase.io/docs/api-crons/) — list and trigger - [Backups API](https://pocketbase.io/docs/api-backups/) — create, upload, download, restore, delete - [Health API](https://pocketbase.io/docs/api-health/) — health check - [Records API](https://pocketbase.io/docs/api-records/) — CRUD, filter, expand, fields - [Realtime API](https://pocketbase.io/docs/api-realtime/) — SSE subscriptions - [Files API](https://pocketbase.io/docs/api-files/) — upload, download, thumbnails - [Auth API](https://pocketbase.io/docs/api-authentication/) — password, OAuth2, OTP, MFA, verification, reset - [Going to Production](https://pocketbase.io/docs/going-to-production/) — self-hosting