Writes PocketBase server-side JavaScript in the Goja JSVM: pb_hooks, routerAdd, record hooks, cron jobs, and synchronous $app APIs. Use for *.pb.js files, pb_hooks, pb_migrations JS, or server-side PocketBase extensions — not the npm JS SDK. Read quirks.md for Goja gotchas (toString, json fields, hoisting, BadRequestError). Always check target PocketBase version: v0.22 and v0.23+ use different JSVM APIs and docs. PocketBase watches pb_hooks JS on disk and hot-reloads hooks inside the running process without exiting.
Writes PocketBase server-side JavaScript in the Goja JSVM: pb_hooks, routerAdd, record hooks, cron jobs, and synchronous $app APIs. Use for *.pb.js files, pb_hooks, pb_migrations JS, or server-side PocketBase extensions — not the npm JS SDK. Read quirks.md for Goja gotchas (toString, json fields, hoisting, BadRequestError). Always check target PocketBase version: v0.22 and v0.23+ use different JSVM APIs and docs. PocketBase watches pb_hooks JS on disk and hot-reloads hooks inside the running process without exiting.
PocketBase JSVM
PocketBase embeds a Goja JavaScript engine for server-side extensions. Code runs inside the PocketBase process — synchronous, no Node/Browser APIs.
Version split — read this first
v0.23 is a breaking JSVM boundary. PocketHost runs both legacy (≤ v0.22) and modern (≥ v0.23) instances. Always confirm the instance PocketBase version before writing or porting hooks — do not mix APIs across versions.
When in doubt, open the JSVM reference for that version — hook names, route handler signatures, and $app methods differ. For mothership v0.39 port work, read v023-upgrade.md and the official JSVM upgrade guide.
Pre-flight checklist
Before writing hook code, verify:
Target PocketBase version — use the matching JSVM docs (≤ v0.22 vs ≥ v0.23)
No async/await, Promises, or .then()
No fetch, setTimeout, setInterval, DOM APIs
No Node built-ins (fs, http, path, etc.)
Use require() for modules (CommonJS only)
Use $app / record APIs — not new PocketBase()
External HTTP via $http.send() (sync), not fetch
$app.store() values that cross requests: JSON.stringify/parse at boundaries — never mutate get() results in place (app-store.md)
Json fields, hook routers, client errors: skim quirks.md if behavior looks like Node
For full environment constraints, see constraints.md. Goja behavioral quirks (toString, hoisting, json fields, error sanitization): quirks.md.
PocketBase watches hook JS on disk (pb_hooks/*.pb.js and files loaded via require()). When they change, it reloads hook code inside the running process — the PocketBase process does not exit. This is separate from PM2 or Docker restarts.
Operational implications:
Do not git checkout, rsync, or deploy while PocketBase is running if the operation swaps hook files incrementally. A mid-checkout tree can be picked up and loaded as a torn mix of old and new code.
Mothership: stop before branch switches or bulk hook deploys (e.g. v39.sh --forward keeps mothership stopped until checkout finishes, then pm2 reload).
Customer instances on PocketHost: FTP/phio deploy restarts the instance container so hooks reload from a consistent tree — different mechanism, same “don’t run torn hooks” goal.
Hook categories
Names differ by version — check the JSVM reference for your target.
Request body (≥ v0.23) — use e.bindBody() + DynamicModel, not JSON.parse(readerToString(e.request.body)):
let data = newDynamicModel({ trusted_ips: [] })
e.bindBody(data)
data = JSON.parse(JSON.stringify(data)) // required before destructuring / $common validatorsconst { trusted_ips } = data
Quick reads: e.requestInfo().body. Raw stream: readerToString(e.request.body) only for webhooks / signature verification.
Concurrent hooks must not share live Goja objects via $app.store(). Use JSON string boundaries and setFunc for atomic read-modify-write. See app-store.md.
Examples
See hooks-examples.md for copy-paste patterns from this repo and PocketHost docs.
API reference
Pick the JSVM reference for the target version (see Version split above).