Skip to main content
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()
})

Build docs developers (and LLMs) love