Extending with Hooks
PocketBase hooks let you run custom JavaScript on the server — right inside your instance. Add API routes, react to record changes, schedule cron jobs, or send emails, all without deploying a separate backend.
On PocketBase Cloud you can write hooks in the portal’s editor, or keep them as files in your repository and push them with the CLI.
Creating a hook
Using the portal
- Open your PocketBase instance in the portal
- Go to the Hooks tab
- Click New Hook, give the file a name ending in
.pb.js(e.g.,main.pb.js) - Write your code and save — the instance reloads hooks automatically
Using the CLI
Keep hook files in your project’s pb_hooks/ directory and push them from
there. In a directory linked to the instance, a deploy is enough:
cd db
pb cloud pb deploy # pushes every pb_hooks/*.pb.js
Or push a directory explicitly, without deploying anything else:
pb cloud pb hooks push ./pb_hooks --name my-app-db
pb cloud pb hooks ls --name my-app-db
pb cloud pb hooks rm old.pb.js --name my-app-db
Only files ending in .pb.js are uploaded, and the instance reloads them
automatically. This is the path to prefer if you want hooks reviewed in pull
requests and testable locally — see
Local Development, where
./pocketbase serve runs the very same pb_hooks/ directory on your machine.
pb cloud pb hooks lslists hooks that were pushed through the CLI or written in the portal. Hooks shipped inside a creation archive run on the instance but aren’t recorded there, so the list can read empty while hooks are live. Push them once to manage them from the CLI.
Example: a custom API route
routerAdd("GET", "/api/hello/{name}", (e) => {
const name = e.request.pathValue("name");
return e.json(200, { message: `Hello ${name}!` });
});
Your route is immediately available at
https://<instance-name>.pocketbasecloud.com/api/hello/world.
Example: react to record changes
Run logic whenever a record is created, updated, or deleted:
onRecordCreate((e) => {
// runs before the record is persisted — you can still modify it
e.record.set("slug", e.record.get("title").toLowerCase().replaceAll(" ", "-"));
e.next();
}, "posts");
onRecordAfterCreateSuccess((e) => {
// runs after the record is saved
console.log("new post:", e.record.id);
e.next();
}, "posts");
Example: scheduled cron jobs
cronAdd("cleanup", "0 3 * * *", () => {
const records = $app.findRecordsByFilter(
"sessions",
"created < @yesterday",
"-created",
200,
0
);
for (const record of records) {
$app.delete(record);
}
});
Example: send an email
onRecordAfterCreateSuccess((e) => {
const message = new MailerMessage({
from: { address: e.app.settings().meta.senderAddress },
to: [{ address: e.record.email() }],
subject: "Welcome!",
html: "<p>Thanks for signing up.</p>",
});
e.app.newMailClient().send(message);
e.next();
}, "users");
What hooks can access
Hooks run in PocketBase’s embedded JavaScript VM with access to:
$app— the full PocketBase app instance (query, create, update records)routerAdd— register custom HTTP routesonRecord*event handlers — before/after create, update, deletecronAdd— cron-style scheduled jobs$http.send()— make outbound HTTP requests to third-party APIs$os,$filesystem,$security— utility namespaces
See the PocketBase JSVM documentation for the complete API.
When to use a backend instead
Hooks are perfect for lightweight logic tied to your data. For heavier workloads — long-running jobs, large dependencies, websockets, or code you want to develop and test locally as a normal project — deploy a dedicated backend alongside your instance.