Comprehensive Pb Hooks
Battle-tested skill for server, side, javascript, hooks. Includes structured workflows, validation checks, and reusable patterns for pocketbase.
Comprehensive PB Hooks
A specialized skill for writing PocketBase server-side JavaScript hooks — covering event hooks, custom API routes, data validation, computed fields, email triggers, external API integration, and background task scheduling using PocketBase's goja-based runtime.
When to Use This Skill
Choose Comprehensive PB Hooks when you need to:
- Add custom business logic triggered by record CRUD events
- Create custom API endpoints beyond PocketBase's auto-generated REST API
- Validate or transform data before it's saved to collections
- Send emails, notifications, or webhook calls in response to events
- Implement computed fields, counters, or aggregate calculations
Consider alternatives when:
- You need collection schema design (use a PB collections skill)
- You need API rules configuration (use a PB API rules skill)
- You need deployment setup (use a PB deploy skill)
Quick Start
// pb_hooks/main.pb.js // IMPORTANT: Files must end with .pb.js // Runtime: goja (ES5.1 + limited ES6, NO async/await, NO arrow functions in older versions) // Record create hook — validate and enrich data onRecordBeforeCreateRequest(function(e) { var record = e.record; // Validate title length if (record.get("title").length < 5) { throw new BadRequestError("Title must be at least 5 characters"); } // Set computed field var slug = record.get("title").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); record.set("slug", slug); // Set default status if (!record.get("status")) { record.set("status", "draft"); } }, "posts"); // After create — send notification onRecordAfterCreateRequest(function(e) { var record = e.record; var author = $app.dao().findRecordById("users", record.get("author")); // Send email notification var message = new MailerMessage(); message.from = { address: $app.settings().meta.senderAddress, name: $app.settings().meta.senderName }; message.to = [{ address: author.get("email") }]; message.subject = "Your post was published"; message.html = "<p>Your post '" + record.get("title") + "' is now live.</p>"; $app.newMailClient().send(message); }, "posts");
Core Concepts
Hook Types
| Hook | Fires When | Use Case |
|---|---|---|
onRecordBeforeCreateRequest | Before record creation | Validation, defaults |
onRecordAfterCreateRequest | After record creation | Notifications, logging |
onRecordBeforeUpdateRequest | Before record update | Validation, computed fields |
onRecordAfterUpdateRequest | After record update | Sync, webhook triggers |
onRecordBeforeDeleteRequest | Before record deletion | Cascade checks, soft delete |
onRecordAfterDeleteRequest | After record deletion | Cleanup, notifications |
onBeforeServe | Before HTTP server starts | Register custom routes |
onModelBeforeCreate | Before model save (lower level) | System-level hooks |
Custom API Routes
// pb_hooks/routes.pb.js onBeforeServe(function(e) { var router = e.router; // Custom GET endpoint router.addRoute("GET", "/api/custom/stats", function(c) { var result = arrayOf(new DynamicModel({ "total": 0, "published": 0, })); $app.dao().db() .newQuery("SELECT COUNT(*) as total, SUM(CASE WHEN status='published' THEN 1 ELSE 0 END) as published FROM posts") .all(result); return c.json(200, { total: result[0].total, published: result[0].published, }); }, $apis.requireAdminAuth()); // Custom POST endpoint with body parsing router.addRoute("POST", "/api/custom/contact", function(c) { var data = $apis.requestInfo(c).data; if (!data.email || !data.message) { throw new BadRequestError("Email and message are required"); } // Save to contacts collection var collection = $app.dao().findCollectionByNameOrId("contacts"); var record = new Record(collection); record.set("email", data.email); record.set("message", data.message); record.set("status", "new"); $app.dao().saveRecord(record); return c.json(200, { success: true }); }); });
Data Validation and Transformation
// pb_hooks/validation.pb.js // Complex validation with cross-collection checks onRecordBeforeCreateRequest(function(e) { var record = e.record; // Check for duplicate slug try { $app.dao().findFirstRecordByData("posts", "slug", record.get("slug")); throw new BadRequestError("A post with this slug already exists"); } catch (err) { if (err.message !== "sql: no rows in result set") { throw err; // Re-throw if not "not found" } } // Rate limiting: max 5 posts per user per day var result = arrayOf(new DynamicModel({ "count": 0 })); $app.dao().db() .newQuery("SELECT COUNT(*) as count FROM posts WHERE author = {:author} AND created >= datetime('now', '-1 day')") .bind({ author: record.get("author") }) .all(result); if (result[0].count >= 5) { throw new BadRequestError("Daily post limit reached (5 per day)"); } }, "posts"); // Auto-compute on update onRecordBeforeUpdateRequest(function(e) { var record = e.record; var original = e.record.originalCopy(); // Track edit count var editCount = (original.get("edit_count") || 0) + 1; record.set("edit_count", editCount); record.set("last_edited_at", new Date().toISOString()); }, "posts");
Configuration
| Parameter | Description | Example |
|---|---|---|
hooks_dir | Directory for hook files | "pb_hooks/" |
file_suffix | Required file extension | ".pb.js" |
runtime | JavaScript runtime engine | "goja" (ES5.1+) |
hot_reload | Auto-reload hooks on file change | true (dev mode) |
collection | Collection to attach hook to | "posts" |
Best Practices
-
Use
onRecordBefore*for validation andonRecordAfter*for side effects — Validation belongs in "before" hooks because you can throw errors to prevent the operation. Notifications, logging, and external API calls belong in "after" hooks because they should only run if the operation succeeded. -
Remember goja limitations: no async/await, limited ES6 — PocketBase uses the goja runtime, not Node.js. Arrow functions work in recent versions but
async/await,import/export, and many ES6+ features don't. Write ES5-style JavaScript withfunction(){}for maximum compatibility. -
Use
e.record.originalCopy()to detect what changed — In update hooks,e.recordhas the new values ande.record.originalCopy()has the previous values. Compare them to determine which fields actually changed and trigger side effects only when relevant fields are modified. -
Handle errors in after-hooks gracefully — If an after-create hook fails (e.g., email service is down), the record has already been created. Don't let side effect failures crash the response. Log the error and implement retry logic or a dead-letter queue for critical operations.
-
Organize hooks by feature, not by type — Put all hooks for the "posts" feature in
pb_hooks/posts.pb.jsrather than splitting before/after hooks across multiple files. This keeps related logic together and makes the codebase navigable.
Common Issues
Hooks don't fire after changing the file — PocketBase only auto-reloads hooks in development mode. In production, you must restart PocketBase after deploying hook changes. Verify your file ends with .pb.js (not .js) — PocketBase ignores files without this suffix.
"Cannot use import statement" or async/await errors — The goja runtime doesn't support ES modules or async/await. Replace import with CommonJS require() patterns, and replace async calls with synchronous alternatives. Use $http.send() for synchronous HTTP requests instead of fetch.
Hook throws error but record is still created — If you're using onRecordAfterCreateRequest and throw an error, the record already exists in the database. Validation and rejection logic must go in onRecordBeforeCreateRequest to prevent the record from being saved.
Reviews
No reviews yet. Be the first to review this template!
Similar Templates
Full-Stack Code Reviewer
Comprehensive code review skill that checks for security vulnerabilities, performance issues, accessibility, and best practices across frontend and backend code.
Test Suite Generator
Generates comprehensive test suites with unit tests, integration tests, and edge cases. Supports Jest, Vitest, Pytest, and Go testing.
Pro Architecture Workspace
Battle-tested skill for architectural, decision, making, framework. Includes structured workflows, validation checks, and reusable patterns for development.