Hooks & custom logic
Run server-side logic on record events — in JavaScript or Go.
Updated
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
appAPI, 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.