PocketBase provides a comprehensive event system that allows you to execute custom logic at specific points in the application lifecycle.
Event types
PocketBase events are categorized into several groups:
- App events - Application lifecycle events
- Model events - Generic database model events
- Record events - Specific to record operations
- Collection events - Collection schema changes
- Request events - HTTP API request events
App events
OnBootstrap
Triggered when initializing app resources:
app.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error {
log.Println("App is bootstrapping")
return e.Next()
})
OnServe
Triggered when the web server starts:
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
// Add custom routes
e.Router.GET("/hello", func(re *core.RequestEvent) error {
return re.String(200, "Hello, World!")
})
return e.Next()
})
OnTerminate
Triggered when the app is being terminated:
app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {
log.Println("App is shutting down")
// Cleanup logic
return e.Next()
})
OnBackupCreate
Triggered when creating a backup:
app.OnBackupCreate().BindFunc(func(e *core.BackupEvent) error {
log.Printf("Creating backup: %s", e.Name)
return e.Next()
})
OnBackupRestore
Triggered before restoring a backup:
app.OnBackupRestore().BindFunc(func(e *core.BackupEvent) error {
log.Printf("Restoring backup: %s", e.Name)
return e.Next()
})
Record events
OnRecordCreate
Triggered when creating a new record:
// For all collections
app.OnRecordCreate().BindFunc(func(e *core.RecordEvent) error {
log.Printf("Creating record in %s", e.Record.Collection().Name)
return e.Next()
})
// For specific collection
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
// Set default values
if e.Record.GetInt("views") == 0 {
e.Record.Set("views", 0)
}
return e.Next()
})
OnRecordUpdate
Triggered when updating a record:
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordEvent) error {
// Track changes
original := e.Record.Original()
if e.Record.GetString("title") != original.GetString("title") {
log.Println("Title was changed")
}
return e.Next()
})
OnRecordDelete
Triggered when deleting a record:
app.OnRecordDelete("posts").BindFunc(func(e *core.RecordEvent) error {
log.Printf("Deleting post: %s", e.Record.GetString("title"))
return e.Next()
})
OnRecordValidate
Triggered during record validation:
app.OnRecordValidate("posts").BindFunc(func(e *core.RecordEvent) error {
// Custom validation
if len(e.Record.GetString("title")) < 5 {
return errors.New("title must be at least 5 characters")
}
return e.Next()
})
Event execution phases
Record events have multiple execution phases:
Before execution
OnRecordCreate, OnRecordUpdate, OnRecordDelete run BEFORE validation and database operations:
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
// Runs BEFORE validation and INSERT
e.Record.Set("created_at", time.Now())
return e.Next()
})
Execute phase
OnRecordCreateExecute, OnRecordUpdateExecute, OnRecordDeleteExecute run right before the database operation:
app.OnRecordCreateExecute("posts").BindFunc(func(e *core.RecordEvent) error {
// Runs after validation, right before INSERT
log.Println("About to insert into database")
return e.Next()
})
After success
OnRecordAfterCreateSuccess, OnRecordAfterUpdateSuccess, OnRecordAfterDeleteSuccess run after successful database persistence:
app.OnRecordAfterCreateSuccess("posts").BindFunc(func(e *core.RecordEvent) error {
// Runs after successful INSERT
// Safe to perform external API calls here
return e.Next()
})
In transactions, “After Success” hooks are delayed until the transaction commits.
After error
OnRecordAfterCreateError, OnRecordAfterUpdateError, OnRecordAfterDeleteError run after failed operations:
app.OnRecordAfterCreateError("posts").BindFunc(func(e *core.RecordErrorEvent) error {
log.Printf("Failed to create record: %v", e.Error)
return e.Next()
})
Collection events
OnCollectionCreate
Triggered when creating a collection:
app.OnCollectionCreate().BindFunc(func(e *core.CollectionEvent) error {
log.Printf("Creating collection: %s", e.Collection.Name)
return e.Next()
})
OnCollectionUpdate
Triggered when updating a collection:
app.OnCollectionUpdate().BindFunc(func(e *core.CollectionEvent) error {
log.Printf("Updating collection: %s", e.Collection.Name)
return e.Next()
})
OnCollectionDelete
Triggered when deleting a collection:
app.OnCollectionDelete().BindFunc(func(e *core.CollectionEvent) error {
if e.Collection.Name == "important" {
return errors.New("cannot delete this collection")
}
return e.Next()
})
Request events
Custom routes
Add custom HTTP endpoints:
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
e.Router.GET("/api/custom", func(re *core.RequestEvent) error {
data := map[string]any{
"message": "Hello from custom route",
}
return re.JSON(200, data)
} /* optional middlewares */)
return e.Next()
})
OnRecordsListRequest
Triggered on listing records via API:
app.OnRecordsListRequest("posts").BindFunc(func(e *core.RecordsListRequestEvent) error {
log.Printf("Listing %d posts", len(e.Records))
return e.Next()
})
OnRecordViewRequest
Triggered when viewing a single record:
app.OnRecordViewRequest("posts").BindFunc(func(e *core.RecordRequestEvent) error {
// Increment view count
e.Record.Set("views+", 1)
app.Save(e.Record)
return e.Next()
})
Hook binding
BindFunc()
Bind a function to an event:
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
// Your logic here
return e.Next()
})
Bind()
Bind a handler with custom ID and priority:
import "github.com/pocketbase/pocketbase/tools/hook"
app.OnRecordCreate("posts").Bind(&hook.Handler[*core.RecordEvent]{
Id: "my-custom-handler",
Priority: 100, // Higher = runs later
Func: func(e *core.RecordEvent) error {
return e.Next()
},
})
Unbind()
Remove a specific handler:
app.OnRecordCreate("posts").Unbind("my-custom-handler")
Event priorities
Control execution order with priorities:
// Runs first (lower priority number)
app.OnRecordCreate("posts").Bind(&hook.Handler[*core.RecordEvent]{
Id: "handler-1",
Priority: -100,
Func: func(e *core.RecordEvent) error {
log.Println("First")
return e.Next()
},
})
// Runs last (higher priority number)
app.OnRecordCreate("posts").Bind(&hook.Handler[*core.RecordEvent]{
Id: "handler-2",
Priority: 100,
Func: func(e *core.RecordEvent) error {
log.Println("Last")
return e.Next()
},
})
Default priority is 0. Lower numbers run earlier, higher numbers run later.
Event propagation
e.Next()
Continue to the next handler or core logic:
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
// Run your logic
log.Println("Before save")
// Continue execution
if err := e.Next(); err != nil {
return err
}
// Run logic after
log.Println("After save")
return nil
})
Stopping propagation
Return an error to stop execution:
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
if e.Record.GetString("status") == "banned" {
return errors.New("cannot create banned posts")
}
return e.Next()
})
Auth events
OnRecordAuthRequest
Triggered after successful authentication:
app.OnRecordAuthRequest("users").BindFunc(func(e *core.RecordAuthRequestEvent) error {
log.Printf("User %s authenticated via %s",
e.Record.Email(),
e.AuthMethod,
)
return e.Next()
})
OnRecordAuthWithPasswordRequest
Triggered during password authentication:
app.OnRecordAuthWithPasswordRequest("users").BindFunc(func(e *core.RecordAuthWithPasswordRequestEvent) error {
// Log login attempts
log.Printf("Login attempt for: %s", e.Identity)
return e.Next()
})
OnRecordAuthWithOAuth2Request
Triggered during OAuth2 authentication:
app.OnRecordAuthWithOAuth2Request("users").BindFunc(func(e *core.RecordAuthWithOAuth2RequestEvent) error {
if e.IsNewRecord {
// Set default values for new OAuth2 users
e.Record.Set("verified", true)
}
return e.Next()
})
Mail events
OnMailerSend
Intercept all outgoing emails:
app.OnMailerSend().BindFunc(func(e *core.MailerEvent) error {
log.Printf("Sending email to: %v", e.Message.To)
return e.Next()
})
OnMailerRecordAuthAlertSend
Intercept auth alert emails:
app.OnMailerRecordAuthAlertSend("users").BindFunc(func(e *core.MailerRecordEvent) error {
// Customize auth alert email
e.Message.Subject = "Security Alert: New Login"
return e.Next()
})
Best practices
Keep hooks focused
// Good - single responsibility
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
return validatePostContent(e.Record)
})
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
return notifySubscribers(e.Record)
})
// Bad - too many responsibilities
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
validatePostContent(e.Record)
notifySubscribers(e.Record)
updateStatistics(e.Record)
sendWebhook(e.Record)
return e.Next()
})
Use appropriate event phases
// Before database operation - modify data
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordEvent) error {
e.Record.Set("slug", generateSlug(e.Record.GetString("title")))
return e.Next()
})
// After successful operation - external actions
app.OnRecordAfterCreateSuccess("posts").BindFunc(func(e *core.RecordEvent) error {
return sendNotification(e.Record) // Safe for external APIs
})
Handle errors gracefully
app.OnRecordAfterCreateSuccess("posts").BindFunc(func(e *core.RecordEvent) error {
if err := sendWebhook(e.Record); err != nil {
// Log but don't fail the request
e.App.Logger().Error("Webhook failed", "error", err)
}
return e.Next()
})