C

Comprehensive Pb Hooks

Battle-tested skill for server, side, javascript, hooks. Includes structured workflows, validation checks, and reusable patterns for pocketbase.

SkillClipticspocketbasev1.0.0MIT
0 views0 copies

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

HookFires WhenUse Case
onRecordBeforeCreateRequestBefore record creationValidation, defaults
onRecordAfterCreateRequestAfter record creationNotifications, logging
onRecordBeforeUpdateRequestBefore record updateValidation, computed fields
onRecordAfterUpdateRequestAfter record updateSync, webhook triggers
onRecordBeforeDeleteRequestBefore record deletionCascade checks, soft delete
onRecordAfterDeleteRequestAfter record deletionCleanup, notifications
onBeforeServeBefore HTTP server startsRegister custom routes
onModelBeforeCreateBefore 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

ParameterDescriptionExample
hooks_dirDirectory for hook files"pb_hooks/"
file_suffixRequired file extension".pb.js"
runtimeJavaScript runtime engine"goja" (ES5.1+)
hot_reloadAuto-reload hooks on file changetrue (dev mode)
collectionCollection to attach hook to"posts"

Best Practices

  1. Use onRecordBefore* for validation and onRecordAfter* 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.

  2. 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 with function(){} for maximum compatibility.

  3. Use e.record.originalCopy() to detect what changed — In update hooks, e.record has the new values and e.record.originalCopy() has the previous values. Compare them to determine which fields actually changed and trigger side effects only when relevant fields are modified.

  4. 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.

  5. Organize hooks by feature, not by type — Put all hooks for the "posts" feature in pb_hooks/posts.pb.js rather 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.

Community

Reviews

Write a review

No reviews yet. Be the first to review this template!

Similar Templates