JavaScript database and filesystem
Use PocketBase records, SQL, transactions, and files safely from server-side JavaScript.
Use the JavaScript bindings when a hook or custom route must query records, execute SQL, group changes in a transaction, or process uploaded files. Keep database work inside the application APIs and validate file input before persisting it.
Before you begin
Create a server-side JavaScript file under pb_hooks and identify the collection and fields your code will access. Run examples against test data. The embedded runtime is not Node.js, so use PocketBase bindings rather than assuming browser or Node filesystem APIs exist.
Read and update records
Use $app to access the current application and its record operations. Retrieve a record, inspect fields with get(), change fields with set(), validate it, and save it through the application.
onRecordAfterUpdateSuccess((e) => {
const title = e.record.get("title")
console.log("Updated", e.record.id, title)
e.next()
}, "posts")Database JSON values are wrapped Go values. Use the record get() and set() helpers instead of treating the value as a native JavaScript object when you read or write JSON fields.
Run SQL
Use the exposed database bindings for SQL when a record API query does not express the operation you need. Bind values instead of concatenating user input into SQL, and keep the query narrow and explicit.
onBootstrap((e) => {
e.next()
const rows = $app.db().newQuery("SELECT id, title FROM posts WHERE status = {:status}").bind({ status: "published" }).all()
console.log("Published posts", rows.length)
})The exact query helper and returned shape depend on the JSVM binding available in the version you run. Confirm the method signature in the generated declarations at pb_data/types.d.ts before shipping code.
Use transactions
Put related record or SQL changes in one application transaction when they must succeed or fail together. Treat an operation inside a transaction as provisional until the transaction commits; an earlier success hook does not prove persistence.
const result = $app.runInTransaction((txApp) => {
const first = txApp.findRecordById("posts", "POST_ID")
first.set("status", "published")
txApp.save(first)
return first.id
})
console.log("Committed", result)POST_ID with a record ID obtained from your own application. Do not use a client-provided ID without checking that the record belongs to the intended operation.Handle files and form data
Use the request event to retrieve uploaded files and the form-data binding to build multipart values. append() keeps multiple values for a key; set() replaces existing values; delete() removes a key.
routerAdd("POST", "/api/myapp/avatar", (e) => {
const files = e.findUploadedFiles("avatar")
if (files.length == 0) throw new BadRequestError("avatar is required")
const form = new FormData()
form.append("owner", e.auth.id)
form.append("avatar", files[0])
return e.json(200, { uploaded: true })
}, $apis.requireAuth())For a raw multipart part, use e.request.formFile("avatar"). For a single uploaded file, findUploadedFiles("avatar") returns ready-to-use filesystem file values. Enforce authentication and collection rules before saving user-controlled files, and do not expose local filesystem paths in responses.
Verify and troubleshoot
Open pb_data/types.d.ts in your editor and confirm the binding names and argument types for the PocketBase version you run.
Run the hook or route with a test record and confirm the expected record, query result, or JSON response.
Send an absent file, invalid record ID, or deliberately rejected validation case. Confirm the handler returns an API error and no partial transaction remains.
Read the record or file through the normal PocketBase API and confirm only the intended fields and file metadata changed.
SQL or binding method is missing
Check pb_data/types.d.ts and the JSVM reference for the version in use. Do not substitute Node.js modules or undocumented globals.
A file upload is empty
Confirm the multipart field name matches the argument to findUploadedFiles() or formFile(), and reject the request when no file is returned.
A transaction appears successful but data is absent
Check the transaction callback for an error and verify after commit. Use an After*Success hook when downstream work must run only after persistence.
Next steps
Continue with JavaScript event hooks to select the correct lifecycle boundary, or JavaScript routes and console commands to expose this logic safely.