pocketbase
PocketBase v0.39+ development - API rules, auth, collections, SDK, realtime, files, Go/JS extending, deployment, production tuning.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
PocketBase v0.39+ development - API rules, auth, collections, SDK, realtime, files, Go/JS extending, deployment, production tuning.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
[Cali] Generate and maintain project AGENTS.md files using semantic analysis. Triggers when: a project has no AGENTS.md, user asks to 'create AGENTS.md', 'generate agents md', 'setup agents md', or when starting work on a project that lacks one. Also triggers when user asks to 'check if AGENTS.md is stale', 'update agents md', or when evolving an existing AGENTS.md. Covers: initial generation from codebase analysis, sem-based staleness detection, pre-commit hooks, and CI guards. Always offer hook installation when invoked for any reason.
[Cali] Validate project AGENTS.md files against best practices and the canonical template. Use when: user says 'validate agents md', 'check agents md quality', 'audit my agents md', or after creating/updating AGENTS.md. Checks: structure, size, content quality, template compliance, and provides fix recommendations. Can offer automatic corrections via ask tool.
[Cali] Tool priority rules for code navigation: Cymbal-first, FFF fallback, batch guidance for token efficiency, and semantic diff workflow. Activates on any file-task within a Git repository to guide agent tool selection.
[Cali] Use this skill whenever the user wants to analyze a codebase and generate a detailed product spec document. Triggers for: "analyze this codebase", "generate a spec", "document this project", "reverse engineer this app", "what does this system do", "create product documentation", "map out the features", "extract product rules from code", "what does this codebase do", "generate product spec from code", "document this app", "spec out this repo", "read this code and tell me what it does", "create a product spec", "reverse engineer the product", "what features does this app have", "document the flows", "extract the prompts from this code", or any request to extract product knowledge from source code. Supports any input method: file uploads, pasted code, filesystem paths, or zip archives. Always use this skill when the user shares code AND wants to understand what the product does — even if they don't explicitly say "spec" or "documentation".
[Cali] Go web stack: Datastar, Templ, DaisyUI, NATS, PocketBase/SQLite, GoAI LLM, Zenflow agents. Scaffold, real-time, hypermedia, auth, DB, embeddings, voice AI, AI agents, durable workflows, queues. Queues use NATS JetStream or goqite. Workflows use go-workflows, turbine, ebind, dagnats, or Hatchet per decision tree below.
Use this skill for any Go (Golang) backend task — writing services, APIs, CLI tools, concurrent code, or reviewing/refactoring existing Go code. Triggers on: any .go file, mention of goroutines, channels, mutexes, Go modules, or requests to build backend systems in Go. Also triggers when setting up linters, running tests, managing Go dependencies, or running the app locally during development.
| name | pocketbase |
| description | PocketBase v0.39+ development - API rules, auth, collections, SDK, realtime, files, Go/JS extending, deployment, production tuning. |
Auto-invoke: triggers on PocketBase, pb_hooks, pb_migrations topics. No external files — all rules inline.
Target: v0.39.1. Verified against current docs. Breaking changes noted inline.
./pocketbase serve # dev on :8090
./pocketbase superuser create admin@x.com pass123
./pocketbase --dir=./mydata serve # custom pb_data
./pocketbase --encryptionEnv=PB_ENCRYPTION_KEY serve
Routes: :8090/ (static pb_public), :8090/_/ (admin), :8090/api/ (REST).
| Type | Use |
|---|---|
| auth | User accounts, any entity that logs in (built-in password, OAuth2, OTP, MFA, token mgmt) |
| base | Regular data: posts, products, orders |
| view | Read-only SQL aggregations (must return id column) |
| Field | When |
|---|---|
text | Strings, optional min/max/regex |
number | Integers/floats, min/max |
bool | true/false |
email | Email validation |
url | URL validation |
date / autodate | Dates (auto on create/update) |
select | Single/multi predefined values |
json | Arbitrary JSON |
file | Attachments, maxSelect/maxSize/mimeTypes/thumbs |
relation | Ref to another collection, cascadeDelete |
editor | Rich text HTML |
geopoint | {lon, lat} — supports geoDistance() in filters |
Index every field used in filter, sort, or API rules. Composite indexes order LTR.
CREATE INDEX idx_posts_author_status ON posts(author, status);
cascadeDelete: true for dependent data (comments on post)cascadeDelete: false for important data (orders, transactions) — blocks deletion// Create
await pb.collection('places').create({
location: { lon: -73.9857, lat: 40.7484 } // lon FIRST
});
// Query nearby
await pb.collection('places').getList(1, 50, {
filter: pb.filter('geoDistance(location, {:point}) <= {:km}', { point: { lon, lat }, km: 5 }),
sort: pb.filter('geoDistance(location, {:point})', { point: { lon, lat } })
});
| Value | Access | Use |
|---|---|---|
null | Locked — superusers only | Admin data, system tables |
"" (empty) | Open to all | Public content |
"expression" | Conditional | Ownership, role checks |
| Field | Use |
|---|---|
@request.auth.id | Authed user ID (empty if guest) |
@request.auth.* | Any auth record field (role, verified) |
@request.body.* | Request body (create/update only) |
@request.query.* | URL params (user-controlled — don't use for authz!) |
@request.context | default, oauth2, otp, password, realtime, protectedFile |
@request.method | HTTP method |
@request.body.field:isset — true if field being sent@request.body.field:changed — true if changed from current@request.body.field:length — array/string length// Owner-only
listRule: 'owner = @request.auth.id'
viewRule: 'owner = @request.auth.id'
createRule: '@request.auth.id != "" && @request.body.owner = @request.auth.id'
updateRule: 'owner = @request.auth.id && @request.body.owner:isset = false'
deleteRule: 'owner = @request.auth.id'
// Public read, authenticated write
listRule: '' viewRule: '' createRule: '@request.auth.id != ""'
// Role-based (@collection cross-lookup)
listRule: '@collection.team_members.user ?= @request.auth.id && @collection.team_members.team ?= team'
// Admin only
listRule: '@request.auth.role = "admin"'
strftime(fmt, dt) — date part extraction (v0.36+): strftime('%Y-%m', created) = "2026-03"length(field) — multi-value counteach(field, expr) — iterate multi-value: each(tags, ? ~ "urgent")issetIf(field, val) — conditional presence checkgeoDistance(geopoint, point) — km distance| Rule fail | HTTP |
|---|---|
| ListRule | 200 empty items |
| View/Update/DeleteRule | 404 (hides existence) |
| CreateRule | 400 |
| Locked rule | 403 |
await pb.collection('users').authWithPassword('email', 'password');
// Generic error on fail — never leak email existence
const { otpId } = await pb.collection('users').requestOTP('user@example.com');
// Always returns otpId (even if email missing) — do not break enumeration protection
const authData = await pb.collection('users').authWithOTP(otpId, '12345678');
requestOTP (built-in rate limiter, label *:requestOTP)authWithOTP consumes the code, invalidates otpId// All-in-one (recommended for web)
await pb.collection('users').authWithOAuth2({ provider: 'google', createData: { name: '' } });
// Manual code exchange (React Native, deep links)
const methods = await pb.collection('users').listAuthMethods();
const provider = methods.oauth2.providers.find(p => p.name === 'google');
// Store provider.state and provider.codeVerifier, redirect to provider.authURL + redirectURL
// In callback: pb.collection('users').authWithOAuth2Code(provider.name, code, codeVerifier, redirectURL)
Redirect URL: https://yourdomain.com/api/oauth2-redirect
// Step 1: auth method A → catches 401 with mfaId
try { await pb.collection('users').authWithPassword('email', 'pass'); }
catch (err) {
const mfaId = err.response?.mfaId;
// Step 2: auth method B + mfaId
await pb.collection('users').authWithOTP(otpId, '12345678', { mfaId });
}
// Superuser generates token for another user
const client = await adminPb.collection('users').impersonate(userId, 3600);
// Non-renewable token! Generate fresh when expired.
pb.authStore.isValid — client-side (JWT expiry)authRefresh() — server verification, returns fresh tokenpb.authStore.loadFromCookie(cookie) / exportToCookie({ httpOnly, secure })LocalAuthStore (browser), AsyncAuthStore (React Native), BaseAuthStore (custom)import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
// Node.js: global.EventSource = EventSource from 'eventsource'
{ requestKey: null }{ requestKey: 'search' }pb.cancelRequest('key') / pb.cancelAllRequests()autoCancellation(false) globally disablesimport { ClientResponseError } from 'pocketbase';
try { ... }
catch (error) {
if (error instanceof ClientResponseError) {
error.status, error.response, error.isAbort;
// Handle 400 (validation), 401, 403, 404, 429
}
}
| Modifier | Field Types | Effect |
|---|---|---|
field+ / +field | relation, file | Append/prepend |
field- | relation, file | Remove |
field+ | number | Increment |
field- | number | Decrement |
await pb.collection('posts').update(id, { 'views+': 1, 'tags+': newTagId });
// ALWAYS use pb.filter() with params — never concatenate
pb.filter('title ~ {:q} && status = {:s}', { q: input, s: 'published' });
pb.beforeSend = (url, options) => { /* modify request */ return { url, options }; };
pb.afterSend = (response, data) => { /* process */ return data; };
// Single request, eliminates N+1
await pb.collection('posts').getList(1, 20, { expand: 'author,category,tags' });
// Nested: { expand: 'author.profile,category.parent' }
// Back-relation: { expand: 'comments_via_post,comments_via_post.author' }
// Back-relation syntax: {referencing_collection}_via_{relation_field}
await pb.collection('posts').getList(1, 20, {
fields: 'id,title,created,expand.author.name'
});
// Excerpt: content:excerpt(200,true) — truncate with ellipsis
// Single record by any field
const user = await pb.collection('users').getFirstListItem(
pb.filter('email = {:e}', { e: email })
);
// Throws 404 if not found
const batch = pb.createBatch();
batch.collection('orders').create({ ...order, id: orderId });
items.forEach(item => batch.collection('order_items').create({ ...item, order: orderId }));
const results = await batch.send(); // all or nothing
⚠ Must be enabled in Settings → Application first.
// Cursor-based (infinite scroll)
const posts = await pb.collection('posts').getList(1, 20, {
skipTotal: true, sort: '-created'
});
const nextCursor = posts.items.at(-1)?.created;
expand for relationsPromise.all// Collection-wide
const unsub = await pb.collection('posts').subscribe('*', (e) => {
e.action; // 'create' | 'update' | 'delete'
e.record;
}, { expand: 'author', fields: 'id,title' });
// Specific record
await pb.collection('posts').subscribe('RECORD_ID', handler);
// Unsubscribe
unsub(); // single callback
pb.collection('posts').unsubscribe(); // all in collection
pb.realtime.unsubscribe(); // all subscriptions
pb.authStore.onChange)*, ViewRule for specific recordpb.realtime.subscribe('PB_CONNECT', (e) => { /* connected, clientId: e.clientId */ });
pb.realtime.onDisconnect = (subscriptions) => { /* handle reconnect */ };
subscribe(recordId) over subscribe('*') for high-trafficfilter, fields, expand to reduce payloadpb.files.getURL(record, record.image);
pb.files.getURL(record, record.image, { thumb: '100x100' });
// Protected files: get token first
const token = await pb.files.getToken();
pb.files.getURL(record, record.file, { token });
Thumbnail formats: WxH (fit), Wx0 (fit width), 0xH (fit height), WxHt (top crop), WxHb (bottom), WxHf (force).
// Plain object (auto-FormData)
await pb.collection('albums').create({ image: fileInput.files[0] });
// Multiple files
await pb.collection('albums').update(id, { images: files });
// Remove
await pb.collection('albums').update(id, { 'images-': filename });
await pb.collection('albums').update(id, { image: null }); // clear all
// Configure in Admin UI or via API:
{
maxSelect: 1,
maxSize: 5242880, // 5MB
mimeTypes: ['image/jpeg', 'image/png'],
thumbs: ['100x100', '200x200']
}
{cdnBase}/{collectionId}/{recordId}/{filename}./pocketbase serve --rateLimiter=true --rateLimiterRPS=10
Strategy (v0.36.7+): fixed-window. Worst case = 2x maxRequests at window boundary. Use Nginx/Caddy in front for defense in depth.
myapp.com {
reverse_proxy 127.0.0.1:8090 { flush_interval -1 }
header { X-Content-Type-Options "nosniff"; Strict-Transport-Security "..."; }
}
Nginx: proxy_buffering off, proxy_read_timeout 3600s (SSE).
await adminPb.backups.create('daily-backup');
await adminPb.backups.getFullList();
await adminPb.backups.restore(key);
Automate with cron; store off-site (S3).
ulimit -n 4096 # open files for realtime connections
GOMEMLIMIT=512MiB # Go GC memory cap in containers
PB_ENCRYPTION="..." # 32-char key for _params encryption at rest
export SMTP_HOST=smtp.sendgrid.net SMTP_PORT=587 SMTP_USER=... SMTP_PASS=...
Never ship no-reply@example.com — configure Meta.senderAddress.
PocketBase defaults (auto-configured): WAL mode, busy_timeout=10s, cache_size=-32MB, foreign_keys=ON.
EXPLAIN QUERY PLANRequires Go 1.25+ (v0.36.7+). No CGO by default.
package main
import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/apis"
)
func main() {
app := pocketbase.New()
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.GET("/api/myapp/hello/{name}", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{"msg": "hello " + e.Request.PathValue("name")})
}).Bind(apis.RequireAuth())
return se.Next()
})
if err := app.Start(); err != nil { log.Fatal(err) }
}
// pb_hooks/main.pb.js — filename must end in .pb.js
/// <reference path="../pb_data/types.d.ts" />
routerAdd("GET", "/api/myapp/hello/{name}", (e) => {
return e.json(200, { message: "Hello " + e.request.pathValue("name") });
}, $apis.requireAuth());
$app, $apis, $os, $security, $filesystem, $dbx, $mails, __hooksrequire() inside handler.require() works, ESM import does notrequire modules from node_modules/ or ${__hooks}/ pathssetTimeout/setInterval, no Node.js fs, no fetch — use $app.newHttpClient()--hooksPool=N)Always call e.Next() (Go) / e.next() (JS) — skipping it silently breaks the chain.
app.OnRecordAfterCreateSuccess("posts").BindFunc(func(e *core.RecordEvent) error {
// Use e.App (not captured outer `app`) — inside a tx, e.App IS txApp
return e.Next()
})
// Bind with Id for later Unbind:
app.OnRecordAfterCreateSuccess("posts").Bind(&hook.Handler{Id: "my-hook", Func: func(e *core.RecordEvent) error {
return e.Next()
}})
app.OnRecordAfterCreateSuccess("posts").Unbind("my-hook")
| Family | Examples | Has request context? |
|---|---|---|
| Model | OnRecordCreate, OnRecord*Success | No — any save path |
| Request | OnRecord*Request, OnRecordsListRequest | Yes — HTTP only |
| Enrich | OnRecordEnrich | Yes — response serialization + realtime SSE |
⚠ Use OnRecordEnrich to hide/redact fields — it applies to realtime events too. Model/request hooks alone leak.
The single most common extending bug: using the wrong app instance. Here is which one is active at each layer of a request.
| Where you are | Use | Why |
|---|---|---|
Route handler (func(e *core.RequestEvent)) | e.App | Top-level app; same object server started with |
Inside RunInTransaction(func(txApp) { ... }) | txApp only | Capturing outer app deadlocks on SQLite writer lock |
Inside a hook fired from a Save inside a tx | e.App | Framework rebinds e.App to txApp automatically |
Inside a hook fired from a non-tx Save | e.App | Points to top-level app (no tx active) |
Inside OnRecordEnrich | e.App | Runs after tx committed — no tx context |
| Cron callback | captured app / se.App | No per-run scoped app; wrap in RunInTransaction if atomicity needed |
| Migration function | the app argument | Already transactional |
Error propagation:
return err inside RunInTransaction → rolls back everything, including audit records written by hooks fired from nested Save callsreturn err from a hook → propagates back through Save → rolls back the txe.Next() → chain broken silently (no error, no realtime broadcast, no enrich pass)err := app.RunInTransaction(func(txApp core.App) error {
// Use ONLY txApp inside — outer app deadlocks (writer lock)
txApp.Save(record1)
txApp.Save(record2)
return nil // commit
// return err // rollback
})
e.App inside hooks is the transactional app when inside a txg := se.Router.Group("/api/myapp")
g.Bind(apis.RequireAuth())
g.Bind(apis.Gzip())
g.POST("/admin/rebuild", handler).Bind(apis.RequireSuperuserAuth())
JS: routerUse("/api/myapp", $apis.requireAuth())
| Middleware | Use |
|---|---|
RequireGuestOnly() | Reject authed clients |
RequireAuth(...collections) | Require auth (opt. restrict to specific auth coll) |
RequireSuperuserAuth() | Superuser only |
RequireSuperuserOrOwnerAuth("id") | Superuser or matching path param |
Gzip() | Compress responses |
BodyLimit(bytes) | Override 32MB default |
SkipSuccessActivityLog() | Suppress activity log |
Default: pure-Go modernc.org/sqlite (no CGO). Use DBConnect for FTS5/ICU/extensions.
⚠ DBConnect called twice: data.db + auxiliary.db.
Option A: ncruces/go-sqlite3 (recommended, no CGO) — pure Go via WASM→Go, suporta ext/unicode, ext/vec1, ext/spellfix1, ext/regexp, FTS5 nativo.
import (
_ "github.com/ncruces/go-sqlite3/driver"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/ext/unicode" // unaccent(), collation
// "github.com/ncruces/go-sqlite3/ext/vec1" // busca vetorial
// "github.com/ncruces/go-sqlite3/ext/spellfix1" // fuzzy matching
)
func init() {
// Auto-registra extensions em toda nova conexão
sqlite3.AutoExtension(unicode.Register)
}
app := pocketbase.NewWithConfig(pocketbase.Config{
DBConnect: func(dbPath string) (*dbx.DB, error) {
return dbx.Open("sqlite3", "file:"+dbPath+";_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)&_pragma=temp_store(MEMORY)&_pragma=cache_size(-32000)")
},
})
Option B: mattn/go-sqlite3 (CGO) — requer CGO_ENABLED=1, perde cross-compilation estática.
import _ "github.com/mattn/go-sqlite3" // CGO
app := pocketbase.NewWithConfig(pocketbase.Config{
DBConnect: func(dbPath string) (*dbx.DB, error) {
return dbx.Open("pb_sqlite3", dbPath)
},
})
Trade-offs ncruces vs modernc:
app.Cron().MustAdd("cleanup-drafts", "0 3 * * *", func() {
app.Logger().Info("cleaning drafts...")
})
// Don't use __pb*__ prefix (reserved for system jobs)
JS: cronAdd("id", "*/30 * * * *", () => { ... })
❌ Leaking NewFilesystem() leaks S3 connections.
✅ Always defer fs.Close() (Go) / try { ... } finally { fs.close() } (JS).
fs, err := app.NewFilesystem()
if err != nil { return err }
defer fs.Close()
Prefer record field API (record.Set("file", f) + app.Save) over direct fs.Upload.
// ❌ String interpolation: filter injection
// ✅ Placeholder binding:
record, err := app.FindFirstRecordByFilter("users", "email = {:email} && verified = true",
dbx.Params{"email": email})
JS: $app.findFirstRecordByFilter("users", "email = {:e}", { e: email })
Go: m.Register(upFn, downFn) in migrations/ package. Auto-discovered when migratecmd.MustRegister(app, ...) is called in main.
JS (pb_migrations):
// pb_migrations/1712500000_add_collection.js
migrate((app) => {
const col = new Collection({ type: "base", name: "audit", fields: [...] });
app.save(col);
}, (app) => {
const col = app.findCollectionByNameOrId("audit");
app.delete(col);
});
<unix>_<description>.jsapp in up/down is transactionalpb_migrations/, never pb_data/meta := e.App.Settings().Meta
from := mail.Address{Name: meta.SenderName, Address: meta.SenderAddress}
msg := &pbmail.Message{From: from, To: []mail.Address{{Address: to}}, Subject: "...", HTML: "..."}
if err := e.App.NewMailClient().Send(msg); err != nil {
e.App.Logger().Error("email failed", "err", err)
}
// Don't return email errors from hooks — don't roll back business txn
app, _ := tests.NewTestApp("test_pb_data")
defer app.Cleanup()
tests.ApiScenario{
Name: "create post",
Method: http.MethodPost,
URL: "/api/collections/posts/records",
Body: strings.NewReader(`{"title":"hi"}`),
ExpectedStatus: 200,
ExpectedEvents: map[string]int{
"OnRecordCreateRequest": 1,
"OnRecordAfterCreateSuccess": 1,
},
TestAppFactory: func(t testing.TB) *tests.TestApp { return app },
}.Test(t)
app.Settings(), never capture at startupsettings := app.Settings() → app.Save(settings)export PB_ENCRYPTION="32-char-exactly" — AES encrypts _paramsOnSettingsReload hook for in-memory cache invalidationposts (base): title(text), slug(text,unique), content(text), excerpt(text), featured_image(file), author(→users), category(→categories), tags(→tags). Rules: public read, author write.
categories (base): name(text,unique), slug(text,unique), description(text).
tags (base): name(text,unique).
products (base): name(text), slug(unique), description(editor), price(number), compare_price(number), sku(text,unique), stock(number), images(file,many), categories(→categories,many). Rules: public read, admin write.
orders (base): orderNumber(text,unique), status(select: pending/paid/shipped/delivered/cancelled), customer(→users), items(json), total(number,required), shippingAddress(editor), notes(editor). Rules: owner view, admin manage.
reviews (base): product(→products), author(→users), rating(number,1-5), content(text). Rules: author write own, public read.
profiles (auth): extends users. displayName(text), bio(text), avatar(file). Rules: owner edit, public view.
posts (base): content(editor), author(→profiles), media(file,many), likes(json). Rules: public read, auth write.
comments (base): post(→posts,cascadeDelete), author(→profiles), content(text). Rules: public read, auth create, author edit.
follows (base): follower(→profiles), following(→profiles), unique index on pair. Rules: auth create own.
projects (base): name(text), description(text), owner(→users), members(→users,many), status(select: active/archived). Rules: member view, owner manage.
tasks (base): title(text), description(editor), status(select: todo/in_progress/done), priority(select: low/medium/high/critical), assignee(→users), project(→projects,cascadeDelete), dueDate(date). Rules: project member view, assignee/owner update.
categories (base): name(text,unique), description(text), sortOrder(number). Rules: public read, admin manage.
threads (base): title(text), content(editor), author(→users), category(→categories,cascadeDelete), pinned(bool), views(number). Rules: public read, auth create, author edit.
posts (base): content(editor), author(→users), thread(→threads,cascadeDelete). Rules: public read, auth create, author edit.
| Pattern | listRule | viewRule | createRule | updateRule | deleteRule |
|---|---|---|---|---|---|
| Public read, auth write | "" | "" | @request.auth.id != "" | owner check | owner check |
| Owner only | owner = @request.auth.id | same | @request.auth.id != "" && @request.body.owner = @request.auth.id | owner = @request.auth.id | owner = @request.auth.id |
| Auth users | @request.auth.id != "" | same | same | same | same |
| Admin only | @request.auth.role = "admin" | same | same | same | same |
| Public read-only | "" | "" | null | null | null |
// roles collection: { user(→users), role(text) }
// resource rules:
listRule: '@collection.roles.user = @request.auth.id && @collection.roles.role = "admin"'
Hide("fieldName") — in OnRecordEnrich hook (Go) or e.record.hide() (JS)@request.body.field:isset = false — prevent field changes via API rulespb.files.getToken() — valid for limited timefinal pb = PocketBase('http://127.0.0.1:8090');
await pb.collection('users').authWithPassword('email', 'pass');
final records = await pb.collection('posts').getList(page: 1, perPage: 20);
final unsub = await pb.collection('posts').subscribe('*', (e) { print(e.record); });
Same API shape as JS SDK. Use AsyncAuthStore for Flutter.
# Export all collections schema as JSON
# (Admin UI > Settings > Export)
# Import via Admin UI or API
// pb_migrations/1712500000_seed_data.js
migrate((app) => {
const col = app.findCollectionByNameOrId("products");
const data = JSON.parse($os.readFile(`${__hooks}/seed/products.json`));
data.forEach(item => {
const r = new Record(col);
Object.entries(item).forEach(([k, v]) => r.set(k, v));
app.save(r);
});
}, (app) => {
// reverse
});
pb.filter() with named params — never string concatenation