Railbase
GPTClaude

Hooks & custom logic

Run server-side logic on record events — in JavaScript or Go.

Updated

Video guide —watch on YouTube ↗

Hooks let you run logic when records change — validate input, derive fields, send notifications, enforce invariants. Write them in JavaScript for quick iteration, or in Go for type safety and performance.

JavaScript hooks

Drop a *.pb.js file in pb_hooks/ (next to where you run the binary; override with RAILBASE_HOOKS_DIR or hooks.dir in railbase.yaml). It's loaded by an embedded JS runtime at boot and hot-reloaded on save:

// pb_hooks/posts.pb.js
$app.onRecordBeforeCreate("posts").bindFunc((e) => {
  const title = (e.record.title || "").trim();
  if (!title) throw new Error("title required");   // abort with 400
  e.record.title = title;                          // mutate before write
  e.next();
});

$app.onRecordAfterCreate("posts").bindFunc((e) => {
  console.log("post created:", e.record.id);
  e.next();
});

Events available: onRecordBeforeCreate / AfterCreate, …BeforeUpdate / AfterUpdate, …BeforeDelete / AfterDelete.

The $app binding also exposes:

$app.routerAdd("GET", "/api/hello/{name}", (e) =>
  e.json(200, { hello: e.pathParam("name") }));   // chi-style {param} paths
$app.cronAdd("nightly", "0 3 * * *", () => { /* … */ });
$app.onRequest((e) => { /* every request */ e.next(); });

Note

A Before hook that throws aborts the write with a 400. An After hook throwing is logged but does not undo the committed write. Each handler is capped at ~5 seconds of wall-clock time. Rename a file to *.disabled to skip it.

Go hooks

For compiled, typed logic, register from your main (see Project setup). The handler types live in pkg/railbase/hooks; a handler receives a context plus the event, reads/mutates ev.Record (a map[string]any), and returns nil to continue or an error (or hooks.ErrReject) to refuse the write with a 400:

import "github.com/railbase/railbase/pkg/railbase/hooks"

cli.ExecuteWith(func(app *railbase.App) {
    app.GoHooks().OnRecordBeforeCreate("posts",
        func(c *hooks.Context, ev *hooks.RecordEvent) error {
            title, _ := ev.Record["title"].(string)
            if title == "" {
                return fmt.Errorf("title required") // → 400 at the REST layer
            }
            ev.Record["title"] = strings.TrimSpace(title) // mutations persist
            return nil
        })
})

Before-hooks run synchronously and may mutate the record; after-hooks run async, after commit. Pass "" as the collection to hook every collection. Use c.Ctx for timeouts and outbound calls.

Which to use

  • JavaScript — fast iteration, no rebuild, hot-reload. Great for validation, small derivations, glue.
  • Go — type safety, access to the full app API, no per-call interpreter overhead. Reach for it for anything performance-sensitive or complex.

For background work that shouldn't block a request, use Jobs & cron instead of doing it inline in a hook.