> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/pocketbase/pocketbase/llms.txt
> Use this file to discover all available pages before exploring further.

# Event hooks

> Hook into PocketBase lifecycle events with JavaScript

PocketBase provides numerous event hooks that allow you to execute custom logic at specific points in the application lifecycle.

## Hook types

Hooks are categorized by when they fire in the lifecycle:

* **Before hooks** - Fire before an operation (can prevent the operation)
* **After hooks** - Fire after a successful operation
* **Error hooks** - Fire when an operation fails

## Record hooks

### Record lifecycle

```javascript theme={null}
// Before record is validated
onRecordValidate((e) => {
  console.log("Validating record in", e.collection.name)
  return e.next()
})

// Before record is created (after validation)
onRecordCreate((e) => {
  console.log("Creating record:", e.record.id)
  return e.next()
})

// During record creation (database transaction)
onRecordCreateExecute((e) => {
  console.log("Executing create for:", e.record.id)
  return e.next()
})

// After successful creation
onRecordAfterCreateSuccess((e) => {
  console.log("Record created successfully:", e.record.id)
  return e.next()
})

// After creation error
onRecordAfterCreateError((e) => {
  console.log("Failed to create record:", e.error)
  return e.next()
})
```

### Filtering by collection

You can filter hooks to specific collections using tags:

```javascript theme={null}
// Only for "posts" collection
onRecordCreate((e) => {
  const record = e.record
  console.log("Creating post:", record.getString("title"))
  return e.next()
}, "posts")

// Multiple collections
onRecordCreate((e) => {
  console.log("Creating record in:", e.collection.name)
  return e.next()
}, "posts", "articles", "pages")
```

### Record create hooks

```javascript theme={null}
// Auto-set author to current user
onRecordCreate((e) => {
  if (e.collection.name === "posts") {
    const authRecord = e.requestInfo()?.auth
    if (authRecord) {
      e.record.set("author", authRecord.id)
    }
  }
  return e.next()
}, "posts")

// Generate slug from title
onRecordCreate((e) => {
  if (e.collection.name === "posts") {
    const title = e.record.getString("title")
    const slug = title
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-|-$/g, "")
    
    e.record.set("slug", slug)
  }
  return e.next()
}, "posts")

// Send notification after creation
onRecordAfterCreateSuccess((e) => {
  if (e.collection.name === "posts") {
    const record = e.record
    const author = $app.findRecordById("users", record.getString("author"))
    
    const message = new MailerMessage({
      from: {
        address: $app.settings().meta.senderAddress,
        name: $app.settings().meta.senderName,
      },
      to: [{address: author.getString("email")}],
      subject: "Post Published",
      html: `<p>Your post "${record.getString("title")}" has been published!</p>`,
    })
    
    $app.newMailClient().send(message)
  }
  return e.next()
}, "posts")
```

### Record update hooks

```javascript theme={null}
// Prevent updating published posts
onRecordUpdate((e) => {
  if (e.collection.name === "posts") {
    const currentStatus = e.record.getString("status")
    const originalStatus = e.record.originalCopy().getString("status")
    
    if (originalStatus === "published" && currentStatus !== "published") {
      throw new BadRequestError("Cannot unpublish a post")
    }
  }
  return e.next()
}, "posts")

// Track changes
onRecordUpdate((e) => {
  if (e.collection.name === "posts") {
    const original = e.record.originalCopy()
    const current = e.record
    
    if (original.getString("title") !== current.getString("title")) {
      console.log("Title changed from:", original.getString("title"))
      console.log("To:", current.getString("title"))
    }
  }
  return e.next()
}, "posts")

// Update timestamp
onRecordUpdate((e) => {
  if (e.collection.name === "posts") {
    e.record.set("updated_at", new DateTime().string())
  }
  return e.next()
}, "posts")
```

### Record delete hooks

```javascript theme={null}
// Prevent deletion of published posts
onRecordDelete((e) => {
  if (e.collection.name === "posts") {
    if (e.record.getString("status") === "published") {
      throw new ForbiddenError("Cannot delete published posts")
    }
  }
  return e.next()
}, "posts")

// Cleanup related data
onRecordAfterDeleteSuccess((e) => {
  if (e.collection.name === "posts") {
    const postId = e.record.id
    
    // Delete all comments for this post
    const comments = arrayOf(new Record())
    $app.findAllRecordsByData("comments", "post", postId, comments)
    
    for (let comment of comments) {
      $app.delete(comment)
    }
  }
  return e.next()
}, "posts")
```

### Record validation hooks

```javascript theme={null}
onRecordValidate((e) => {
  if (e.collection.name === "posts") {
    const title = e.record.getString("title")
    
    // Check for spam
    const spamWords = ["viagra", "casino", "lottery"]
    for (let word of spamWords) {
      if (title.toLowerCase().includes(word)) {
        throw new BadRequestError("Content flagged as spam")
      }
    }
    
    // Check for duplicate titles
    const existing = $app.findFirstRecordByData("posts", "title", title)
    if (existing && existing.id !== e.record.id) {
      throw new BadRequestError("A post with this title already exists")
    }
  }
  return e.next()
}, "posts")
```

## API request hooks

### Record request hooks

```javascript theme={null}
// Before record create request
onRecordCreateRequest((e) => {
  console.log("Create request for:", e.collection.name)
  console.log("Request data:", e.requestInfo().data)
  return e.next()
})

// Before record update request
onRecordUpdateRequest((e) => {
  // Only allow updating own records
  const authRecord = e.requestInfo().auth
  if (!authRecord) {
    throw new UnauthorizedError()
  }
  
  if (e.record.getString("author") !== authRecord.id) {
    throw new ForbiddenError("You can only update your own records")
  }
  
  return e.next()
}, "posts")

// Before record delete request
onRecordDeleteRequest((e) => {
  // Admins only
  const authRecord = e.requestInfo().auth
  if (!authRecord || authRecord.getString("role") !== "admin") {
    throw new ForbiddenError("Only admins can delete records")
  }
  
  return e.next()
}, "posts")

// Before record view request
onRecordViewRequest((e) => {
  // Track views
  const record = e.record
  record.set("view_count", record.getInt("view_count") + 1)
  $app.save(record)
  
  return e.next()
}, "posts")

// Before records list request
onRecordsListRequest((e) => {
  console.log("Listing records from:", e.collection.name)
  console.log("Filters:", e.request.url.query.get("filter"))
  return e.next()
})
```

### Authentication hooks

```javascript theme={null}
// Before auth request
onRecordAuthRequest((e) => {
  console.log("Auth request for collection:", e.collection.name)
  return e.next()
})

// After successful password auth
onRecordAuthWithPasswordRequest((e) => {
  const record = e.record
  
  // Update last login timestamp
  record.set("last_login", new DateTime().string())
  $app.save(record)
  
  // Log login
  console.log("User logged in:", record.getString("email"))
  
  return e.next()
}, "users")

// After OAuth2 auth
onRecordAuthWithOAuth2Request((e) => {
  const record = e.record
  const provider = e.providerName
  
  console.log("OAuth2 login:", record.getString("email"), "via", provider)
  
  return e.next()
}, "users")

// After OTP auth
onRecordAuthWithOTPRequest((e) => {
  const record = e.record
  console.log("OTP login:", record.getString("email"))
  return e.next()
}, "users")
```

### Email hooks

```javascript theme={null}
// Before verification request
onRecordRequestVerificationRequest((e) => {
  const record = e.record
  console.log("Verification requested for:", record.getString("email"))
  return e.next()
}, "users")

// After verification confirmed
onRecordConfirmVerificationRequest((e) => {
  const record = e.record
  
  // Send welcome email
  const message = new MailerMessage({
    from: {
      address: $app.settings().meta.senderAddress,
      name: $app.settings().meta.senderName,
    },
    to: [{address: record.getString("email")}],
    subject: "Welcome!",
    html: "<p>Thanks for verifying your email!</p>",
  })
  
  $app.newMailClient().send(message)
  
  return e.next()
}, "users")

// Before password reset request
onRecordRequestPasswordResetRequest((e) => {
  console.log("Password reset requested for:", e.record.getString("email"))
  return e.next()
}, "users")

// Before email change request
onRecordRequestEmailChangeRequest((e) => {
  const record = e.record
  const newEmail = e.requestInfo().data.newEmail
  
  console.log("Email change:", record.getString("email"), "->", newEmail)
  
  return e.next()
}, "users")
```

## Collection hooks

```javascript theme={null}
// Before collection create
onCollectionCreate((e) => {
  console.log("Creating collection:", e.collection.name)
  return e.next()
})

// After collection created
onCollectionAfterCreateSuccess((e) => {
  console.log("Collection created:", e.collection.name)
  return e.next()
})

// Before collection update
onCollectionUpdate((e) => {
  const original = e.collection.originalCopy()
  const current = e.collection
  
  if (original.name !== current.name) {
    console.log("Renaming collection:", original.name, "->", current.name)
  }
  
  return e.next()
})

// Before collection delete
onCollectionDelete((e) => {
  console.log("Deleting collection:", e.collection.name)
  return e.next()
})
```

## Application hooks

### Serve hook

```javascript theme={null}
// Runs when the server starts
onServe((e) => {
  console.log("Server starting...")
  
  // Add custom routes
  e.router.GET("/health", (e) => {
    return e.json(200, {status: "ok"})
  })
  
  return e.next()
})
```

### Settings hooks

```javascript theme={null}
// Before settings update
onSettingsUpdateRequest((e) => {
  console.log("Settings being updated")
  return e.next()
})

// After settings reload
onSettingsReload((e) => {
  console.log("Settings reloaded")
  return e.next()
})
```

### Batch hooks

```javascript theme={null}
// Before batch request
onBatchRequest((e) => {
  console.log("Batch request with", e.batch.requests.length, "requests")
  return e.next()
})
```

## File hooks

```javascript theme={null}
// Before file download
onFileDownloadRequest((e) => {
  console.log("Downloading file:", e.servedName)
  console.log("From record:", e.record.id)
  return e.next()
})

// Modify file token generation
onFileBeforeTokenRequest((e) => {
  // Add custom validation before generating file token
  const authRecord = e.requestInfo().auth
  
  if (!authRecord) {
    throw new UnauthorizedError("Authentication required")
  }
  
  return e.next()
})
```

## Realtime hooks

```javascript theme={null}
// Before realtime connect
onRealtimeConnectRequest((e) => {
  console.log("Realtime connection from:", e.realIP())
  return e.next()
})

// Before realtime disconnect
onRealtimeDisconnectRequest((e) => {
  console.log("Realtime disconnection")
  return e.next()
})

// Before subscribe
onRealtimeSubscribeRequest((e) => {
  const subscriptions = e.subscriptions
  console.log("Subscribing to:", subscriptions)
  return e.next()
})
```

## Hook execution order

For a record create operation, hooks fire in this order:

1. `onRecordCreateRequest` - API request starts
2. `onRecordValidate` - Validation runs
3. `onRecordCreate` - Before database operation
4. `onRecordCreateExecute` - During database transaction
5. `onRecordAfterCreateSuccess` - After successful creation

If any hook throws an error:

* Later hooks in the chain won't execute
* `onRecordAfterCreateError` will fire instead of success hooks
* Database changes are rolled back

## Best practices

### Keep hooks focused

```javascript theme={null}
// Good - one responsibility
onRecordCreate((e) => {
  if (e.collection.name === "posts") {
    e.record.set("slug", generateSlug(e.record.getString("title")))
  }
  return e.next()
}, "posts")

// Separate hook for different responsibility
onRecordAfterCreateSuccess((e) => {
  if (e.collection.name === "posts") {
    sendNotification(e.record)
  }
  return e.next()
}, "posts")
```

### Handle errors gracefully

```javascript theme={null}
onRecordAfterCreateSuccess((e) => {
  if (e.collection.name === "posts") {
    try {
      // Send notification (non-critical)
      sendNotification(e.record)
    } catch (err) {
      // Log but don't fail the request
      console.log("Failed to send notification:", err.message)
    }
  }
  return e.next()
}, "posts")
```

### Use collection tags

```javascript theme={null}
// Good - specific collections only
onRecordCreate((e) => {
  // Logic here
  return e.next()
}, "posts", "articles")

// Bad - checks collection name inside
onRecordCreate((e) => {
  if (e.collection.name === "posts" || e.collection.name === "articles") {
    // Logic here
  }
  return e.next()
})
```
