Files
shiftcraft/.claude/skills/pocketbase-best-practices/rules/ext-hooks-record-vs-request.md
2026-04-17 23:26:01 +00:00

3.0 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Pick the Right Record Hook - Model vs Request vs Enrich HIGH Wrong hook = missing request context, double-fired logic, or leaked fields in realtime events hooks, onRecordEnrich, onRecordRequest, model-hooks, extending

Pick the Right Record Hook - Model vs Request vs Enrich

PocketBase v0.23+ splits record hooks into three families. Using the wrong one is the #1 source of "my hook doesn't fire" and "my hidden field still shows up in realtime events" bugs.

Family Examples Fires for Has request context?
Model hooks OnRecordCreate, OnRecordAfterCreateSuccess, OnRecordValidate Any save path - Web API and cron jobs, custom commands, migrations, calls from other hooks No - e.Record, e.App, no e.RequestInfo
Request hooks OnRecordCreateRequest, OnRecordsListRequest, OnRecordViewRequest Only the built-in Web API endpoints Yes - e.RequestInfo, e.Auth, HTTP headers/body
Enrich hook OnRecordEnrich Every response serialization, including realtime SSE events and apis.enrichRecord Yes, via e.RequestInfo

Incorrect (hiding a field in the request hook - leaks in realtime):

// ❌ Only runs for GET /api/collections/users/records/{id}.
//    Realtime SSE subscribers still receive the "role" field.
app.OnRecordViewRequest("users").BindFunc(func(e *core.RecordRequestEvent) error {
    e.Record.Hide("role")
    return e.Next()
})

Correct (use OnRecordEnrich so realtime and list responses also hide the field):

app.OnRecordEnrich("users").BindFunc(func(e *core.RecordEnrichEvent) error {
    e.Record.Hide("role")

    // Add a computed field only for authenticated users
    if e.RequestInfo.Auth != nil {
        e.Record.WithCustomData(true) // required to attach non-schema data
        e.Record.Set("isOwner", e.Record.Id == e.RequestInfo.Auth.Id)
    }
    return e.Next()
})
// JSVM
onRecordEnrich((e) => {
    e.record.hide("role");

    if (e.requestInfo.auth?.collection()?.name === "users") {
        e.record.withCustomData(true);
        e.record.set("computedScore",
            e.record.get("score") * e.requestInfo.auth.get("base"));
    }
    e.next();
}, "users");

Selection guide:

  • Need to mutate the record before any save (API, cron, migration, nested hook)? → OnRecordCreate / OnRecordUpdate (pre-save) or OnRecord*Success (post-save).
  • Need access to HTTP headers, query params, or the authenticated client? → OnRecord*Request.
  • Need to hide fields, redact values, or attach computed props on responses including realtime? → OnRecordEnrich - this is the safest default for response shaping.
  • Need to validate before save? → OnRecordValidate (proxy over OnModelValidate).

Reference: Go Record request hooks · JS Record model hooks