PocketBase Hooks

9 min readUpdated 27 Apr 2026

PocketBase v0.36 includes an embedded JavaScript VM (JSVM) that executes hook files from the pb_hooks/ directory. Hooks allow you to run logic directly inside the PocketBase process — before and after collection CRUD events, on a cron schedule, or at a custom HTTP route. For VeloCMS, hooks are the right tool for database-level side effects that cannot wait for Next.js (e.g. denormalization, internal consistency maintenance). They are the wrong tool for business logic that needs npm packages, fetch, or React.

The JSVM runtime

PocketBase's JSVM is goja — a Go-based ECMAScript 5.1+ implementation with select ES2020+ extensions. It is not Node.js. It does not have access to the Node.js standard library, npm modules, or browser globals. It provides PocketBase-specific globals ($app, $http, $os, etc.) and access to all PocketBase Go packages via thin JavaScript bindings.

Available in JSVMNOT available in JSVM
$app — PocketBase app instancerequire() / import (no module system)
$http.send() — outbound HTTPnpm packages
$os.getenv() — environment variablesNode.js fs, crypto, path, etc.
$mails.send() — email via PB mailerfetch() (use $http.send instead)
cronAdd() — cron schedulerasync/await (use callbacks)
routerAdd() — custom HTTP routesPromise
onRecordCreate/Update/Delete hooksStreams or Buffer

Hook file structure

All hook files live in pb/pb_hooks/. PocketBase loads every .js file in that directory on startup. Each file should focus on one concern — there is no module system to split files, so naming convention is the only organization tool. VeloCMS uses the pattern {collection}-{concern}.pb.js (e.g. posts-denormalize.pb.js, members-sync.pb.js).

pb/pb_hooks/example-hook.pb.js
// Runs BEFORE a post record is created.
// Use e.record to read/modify the record before save.
// Call e.next() to continue — omitting it blocks the operation.
onRecordCreateRequest(function(e) {
  // Auto-set published_at when status transitions to "published"
  if (e.record.get("status") === "published" && !e.record.get("published_at")) {
    e.record.set("published_at", new Date().toISOString());
  }
  e.next();
}, "posts");

// Runs AFTER a post record is updated.
onRecordUpdateRequest(function(e) {
  e.next(); // Must call next() even in after-hooks
  // Log to console — visible in PocketBase server output
  console.log("post updated:", e.record.id);
}, "posts");
javascript

Available hook events

HookWhen it firesCollection arg
onRecordCreateRequestBefore record create, can modify/blockRequired
onRecordAfterCreateRequestAfter record create, record has IDRequired
onRecordUpdateRequestBefore record updateRequired
onRecordAfterUpdateRequestAfter record updateRequired
onRecordDeleteRequestBefore record delete, can blockRequired
onRecordAfterDeleteRequestAfter record deleteRequired
onModelBeforeCreateBefore any model create (lower level)None
onTerminateOn PocketBase shutdownNone

Cron jobs in hooks

PocketBase hooks support cron scheduling via cronAdd(). The cron expression follows standard 5-field format. Cron jobs run inside the PocketBase process and have access to all JSVM globals. They are suitable for lightweight maintenance tasks like expiring old records or sending status pings.

pb/pb_hooks/cleanup-cron.pb.js
// Run every day at 03:00 UTC
cronAdd("cleanup-expired-tokens", "0 3 * * *", function() {
  var expiryThreshold = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
  try {
    var records = $app.dao().findRecordsByFilter(
      "verification_tokens",
      "expires_at < {:threshold}",
      "-created",
      100,
      0,
      { threshold: expiryThreshold }
    );
    records.forEach(function(record) {
      $app.dao().deleteRecord(record);
    });
  } catch (err) {
    console.error("cleanup cron error:", err);
  }
});
javascript

Custom HTTP routes

routerAdd() registers a custom HTTP route on the PocketBase server. This is useful for lightweight webhooks or internal endpoints that need direct database access without going through Next.js. The route handler receives an echo.Context (the underlying Go web framework), and you access request/response via e.request() and e.json().

pb/pb_hooks/internal-route.pb.js
routerAdd("GET", "/api/internal/health", function(e) {
  e.json(200, { status: "ok", ts: new Date().toISOString() });
}, /* optional middleware */ );
javascript

Outbound HTTP from hooks

Use $http.send() to make outbound HTTP calls from a hook. The function is synchronous — it blocks the hook goroutine until complete. Keep outbound calls short (under 5 seconds) to avoid blocking other PocketBase operations. For long-running side effects, post a message to a queue or write a job record to PocketBase and let Next.js pick it up via a polling cron.

$http.send() example
// Notify Next.js of a record change (fire-and-forget style)
onRecordAfterCreateRequest(function(e) {
  e.next();
  try {
    $http.send({
      url: $os.getenv("NEXTJS_INTERNAL_URL") + "/api/internal/notify",
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ collection: "posts", id: e.record.id }),
      timeout: 3, // seconds
    });
  } catch (err) {
    // Non-fatal — do not block the hook response
    console.warn("notify failed:", err);
  }
}, "posts");
javascript

Security model

Hook files run with full access to the PocketBase database — they bypass API rules. This makes hooks powerful but dangerous: a hook that reads from a collection without checking tenant_id can accidentally expose cross-tenant data. Always filter by tenant_id in hook database queries, just as you would in application code. Never expose raw secrets from $os.getenv() in HTTP responses or logs.

  • Hooks bypass PocketBase API rules — filter by tenant_id explicitly in every query
  • Never log raw secrets obtained from $os.getenv()
  • Keep hook files focused — one concern per file, named {collection}-{concern}.pb.js
  • Keep outbound HTTP calls under 5 seconds — long blocks degrade all PocketBase operations
  • For logic requiring npm packages: write to a job record, pick it up from Next.js
  • Do not use $os.exec() — arbitrary shell execution in the PocketBase process is unsafe
  • Test hooks by running the PocketBase binary locally: ./pocketbase serve --dir pb_data

When to use hooks vs Next.js

Use hooks whenUse Next.js server actions / route handlers when
Logic must run inside the DB transactionLogic needs npm packages
Simple denormalization on saveComplex business logic with multiple conditions
Lightweight scheduled cleanupLogic needs Resend, Stripe, or other SDKs
Internal PocketBase-only operationsLogic needs React Email rendering
You need to block a record save (validation)You need to respond to a user action (button click)

Reference

ResourceLink
pb/pb_hooks/All hook files in the VeloCMS repo
PocketBase JSVM docshttps://pocketbase.io/docs/js-overview/
PocketBase hooks referencehttps://pocketbase.io/docs/js-collections/
Cron expression formatStandard 5-field: minute hour day month weekday