betavoidbase is in public beta. It runs, the API is PocketBase's and is not moving, and the version is still 0.x for everything around that.Help us get it to 1.0

pb_hooks

Server-side JavaScript that runs inside the instance: endpoints the records API does not have, handlers that fire around writes, and work on a schedule. This is where a project stops being a database with an API and starts being your application.

The shape of it

Any file ending .pb.js in the directory is loaded, in filename order. There is no build step and no imports to set up: the functions below are globals.

/// <reference path="../pb_data/types.d.ts" />

// a new endpoint, for anything the records API does not already do
routerAdd("GET", "/api/me/summary", (e) => {
  const posts = $app.countRecords("posts", $dbx.hashExp({ author: e.auth.id }));
  return e.json(200, { email: e.auth.email(), posts });
}, $apis.requireAuth());

// something that happens whenever a record is written
onRecordCreateRequest((e) => {
  e.record.set("slug", e.record.get("title").toLowerCase().replaceAll(" ", "-"));
  e.next();                       // run the rest of the handlers, then the write itself
}, "posts");

// and something that happens on a schedule
cronAdd("digest", "0 8 * * *", () => {
  const users = $app.findRecordsByFilter("users", "verified = true", "-created", 100, 0);
  $app.logger().info("digest", "count", users.length);
});

The reference comment on the first line is what makes an editor autocomplete all of it. The file it points at is generated into pb_data on the first run, so run the server once before you start writing.

The three things you will write

Endpoints

routerAdd(method, path, handler, ...middleware). The handler gets a request event: e.auth is the signed-in record or null, e.pathParam("id") reads a path segment, e.bindBody(obj) parses the body, and e.json(status, data) answers. $apis.requireAuth() and $apis.requireSuperuserAuth() are the guards.

Event handlers

Every on* function registers a handler, and trailing arguments limit it to named collections. Calling e.next() runs the remaining handlers and then the action itself, so anything before that call happens first and anything after it happens once the write has gone through. Throwing instead of calling it refuses the request:

onRecordUpdateRequest((e) => {
  if (e.record.get("locked") && !e.auth?.isSuperuser()) {
    throw new ForbiddenError("this post is locked");
  }
  e.next();
}, "posts");

The families are the ones PocketBase has: records both as models and as requests, collections, auth, files, realtime, settings, mail and batches, each with Validate, the action itself, and After...Success / After...Error variants.

Scheduled work

cronAdd(name, expression, handler), with a standard five-field expression. On Cloudflare these become the Worker's own scheduled triggers, so they run whether or not anyone is visiting.

What is available inside

GlobalWhat you get
$appThe database: findRecordById, findFirstRecordByFilter, findRecordsByFilter, countRecords, expandRecord, save, delete, plus settings(), logger() and newMailClient().
$apisRoute guards: requireAuth, requireSuperuserAuth, requireGuestOnly.
$httpsend({url, method, body, headers}), for calling other services.
$securityrandomString, sha256, and friends.
$os.getenvConfiguration, which is what pb_secrets declares.
$dbxexp and hashExp, for building query expressions.

Sharing code between files

require resolves against the hooks directory, and __hooks is its path, so helpers live wherever you like inside it:

// pb_hooks/lib/slug.js
module.exports = { slugify: (s) => s.toLowerCase().trim().replaceAll(/[^a-z0-9]+/g, "-") };

// pb_hooks/posts.pb.js
const { slugify } = require(`${__hooks}/lib/slug.js`);

One difference from PocketBase worth knowing: on Cloudflare the hooks are compiled into the Worker when you deploy, so a change to a hook needs a deploy rather than a restart. Locally, voidbase serve --dev restarts on save.

Where to read on

The hook API is PocketBase's, and every function above is documented under Extend with JavaScript, with the full event list under Event hooks.

Found something wrong on this page?

Fix it yourself. The link below opens this file in GitHub's editor and forks the repository for you if you need one, and your change becomes a pull request without leaving the browser.