> ## 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.

# JavaScript hooks

> Extend PocketBase using JavaScript with the JSVM plugin for rapid development

PocketBase supports extending functionality using JavaScript through the JSVM (JavaScript Virtual Machine) plugin. This allows you to write hooks in JavaScript that run inside your PocketBase instance.

## Overview

JavaScript hooks provide:

* **Hot reloading**: Changes to your hooks are detected automatically (in dev mode)
* **No compilation**: Write JavaScript and see results immediately
* **Full API access**: Access to all PocketBase APIs through global bindings
* **TypeScript support**: Full TypeScript definitions for autocomplete and type checking
* **npm packages**: Use Node.js modules via `require()`

## Setup

<Steps>
  <Step title="Enable JSVM plugin">
    The JSVM plugin is included in the default PocketBase executable. If you're building from source, ensure it's registered:

    ```go main.go theme={null}
    import (
        "github.com/pocketbase/pocketbase"
        "github.com/pocketbase/pocketbase/plugins/jsvm"
    )

    func main() {
        app := pocketbase.New()
        
        jsvm.MustRegister(app, jsvm.Config{
            HooksDir:      "./pb_hooks",
            HooksWatch:    true,
            HooksPoolSize: 15,
        })
        
        app.Start()
    }
    ```
  </Step>

  <Step title="Create hooks directory">
    Create a `pb_hooks` directory in your project root:

    ```bash theme={null}
    mkdir pb_hooks
    ```
  </Step>

  <Step title="Create a hook file">
    Create a file ending in `.pb.js` or `.pb.ts`:

    ```bash theme={null}
    touch pb_hooks/main.pb.js
    ```
  </Step>

  <Step title="Write your first hook">
    Add some code to your hook file:

    ```javascript pb_hooks/main.pb.js theme={null}
    onRecordCreate((e) => {
        console.log("Record created:", e.record.get("id"))
    })
    ```
  </Step>
</Steps>

## Configuration

The JSVM plugin accepts the following configuration options:

| Option              | Type     | Default                        | Description                           |
| ------------------- | -------- | ------------------------------ | ------------------------------------- |
| `HooksDir`          | string   | `pb_data/../pb_hooks`          | Directory containing JavaScript hooks |
| `HooksWatch`        | boolean  | `false`                        | Enable auto-restart on file changes   |
| `HooksFilesPattern` | string   | `^.*(\\.pb\\.js\|\\.pb\\.ts)$` | Regex pattern for hook files          |
| `HooksPoolSize`     | int      | `0`                            | Number of pre-warmed JS runtimes      |
| `MigrationsDir`     | string   | `pb_data/../pb_migrations`     | Directory for JS migrations           |
| `OnInit`            | function | `nil`                          | Callback to register custom bindings  |

<Info>
  Set `HooksPoolSize` to a positive number (e.g., 15) to improve performance by pre-warming JavaScript runtime instances.
</Info>

## Global bindings

PocketBase exposes several global objects and functions to your JavaScript hooks:

### Core objects

| Binding       | Description                         |
| ------------- | ----------------------------------- |
| `$app`        | The PocketBase app instance         |
| `$http`       | HTTP client for making requests     |
| `$os`         | Operating system utilities          |
| `$security`   | Security and cryptography functions |
| `$filepath`   | File path manipulation              |
| `$filesystem` | File system operations              |
| `$mails`      | Email sending utilities             |
| `$apis`       | API helpers and middlewares         |
| `$dbx`        | Database query builder helpers      |
| `$template`   | Template rendering                  |

### Hook registration functions

All event hooks are available as global functions:

```javascript theme={null}
// Model events
onModelCreate((e) => { /* ... */ })
onModelUpdate((e) => { /* ... */ })
onModelDelete((e) => { /* ... */ })

// Record events
onRecordCreate((e) => { /* ... */ }, "posts") // Optional tags
onRecordUpdate((e) => { /* ... */ })
onRecordDelete((e) => { /* ... */ })

// Auth events
onRecordAuthRequest((e) => { /* ... */ })
onRecordAuthWithPasswordRequest((e) => { /* ... */ })
onRecordAuthWithOAuth2Request((e) => { /* ... */ })

// Settings events
onSettingsListRequest((e) => { /* ... */ })
onSettingsUpdateRequest((e) => { /* ... */ })

// More events...
```

<Note>
  See the [Event hooks reference](/extending/event-hooks) for a complete list of available events.
</Note>

## Examples

### Basic record hook

```javascript pb_hooks/posts.pb.js theme={null}
// Validate post title before creation
onRecordCreate((e) => {
    const title = e.record.get("title")
    
    if (title.length < 5) {
        throw new BadRequestError("Title must be at least 5 characters")
    }
}, "posts")

// Log post updates
onRecordUpdate((e) => {
    console.log("Post updated:", e.record.get("id"))
}, "posts")
```

### Send email on record creation

```javascript pb_hooks/notifications.pb.js theme={null}
onRecordCreate((e) => {
    const message = new MailerMessage({
        from: {
            address: $app.settings().meta.senderAddress,
            name: $app.settings().meta.senderName,
        },
        to: [{address: e.record.get("email")}],
        subject: "Welcome!",
        html: "<p>Thanks for signing up!</p>",
    })
    
    $app.newMailClient().send(message)
}, "users")
```

### Custom validation

```javascript pb_hooks/validation.pb.js theme={null}
onRecordCreate((e) => {
    const age = e.record.get("age")
    const email = e.record.get("email")
    
    // Age validation
    if (age < 18) {
        throw new BadRequestError("You must be at least 18 years old")
    }
    
    // Email domain validation
    if (!email.endsWith("@company.com")) {
        throw new BadRequestError("Only company emails are allowed")
    }
}, "employees")
```

### HTTP request

```javascript pb_hooks/webhooks.pb.js theme={null}
onRecordCreate((e) => {
    const response = $http.send({
        url: "https://api.example.com/webhook",
        method: "POST",
        body: JSON.stringify({
            event: "record.created",
            record: e.record,
        }),
        headers: {
            "content-type": "application/json",
            "authorization": "Bearer SECRET_TOKEN",
        },
        timeout: 10, // seconds
    })
    
    console.log("Webhook response:", response.statusCode)
}, "posts")
```

### Database queries

```javascript pb_hooks/stats.pb.js theme={null}
onRecordCreate((e) => {
    // Count total posts by this author
    const authorId = e.record.get("author")
    const count = $app.findRecordsByFilter(
        "posts",
        `author = {:author}`,
        "-created",
        100,
        0,
        {author: authorId}
    ).length
    
    // Update author's post count
    const author = $app.findRecordById("authors", authorId)
    author.set("postCount", count)
    $app.save(author)
}, "posts")
```

### Modify record before saving

```javascript pb_hooks/slugify.pb.js theme={null}
onRecordCreate((e) => {
    // Auto-generate slug from title
    const title = e.record.get("title")
    const slug = title
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-|-$/g, "")
    
    e.record.set("slug", slug)
}, "posts")

onRecordUpdate((e) => {
    // Update slug if title changed
    if (e.record.originalCopy().get("title") !== e.record.get("title")) {
        const title = e.record.get("title")
        const slug = title
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, "-")
            .replace(/^-|-$/g, "")
        
        e.record.set("slug", slug)
    }
}, "posts")
```

### Add custom routes

```javascript pb_hooks/routes.pb.js theme={null}
routerAdd("GET", "/api/hello", (e) => {
    return e.json(200, {message: "Hello from JavaScript!"})
})

routerAdd("POST", "/api/stats/:collection", (e) => {
    const collection = e.request.pathValue("collection")
    
    const count = $app.findRecordsByFilter(
        collection,
        "",
        "-created",
        1,
        0
    ).totalItems
    
    return e.json(200, {
        collection: collection,
        totalRecords: count,
    })
}, $apis.requireAuth())
```

### Scheduled tasks

```javascript pb_hooks/cron.pb.js theme={null}
// Run every day at midnight
cronAdd("cleanup", "0 0 * * *", () => {
    // Delete old records
    const cutoff = new DateTime()
    cutoff.addDate(0, 0, -30) // 30 days ago
    
    const oldRecords = $app.findRecordsByFilter(
        "logs",
        `created < {:cutoff}`,
        "-created",
        100,
        0,
        {cutoff: cutoff.string()}
    )
    
    oldRecords.forEach((record) => {
        $app.delete(record)
    })
    
    console.log(`Deleted ${oldRecords.length} old log records`)
})
```

## TypeScript support

PocketBase automatically generates TypeScript definitions in `pb_data/types.d.ts`. Use `.pb.ts` extensions for full type checking:

```typescript pb_hooks/typed.pb.ts theme={null}
/// <reference path="../pb_data/types.d.ts" />

onRecordCreate((e) => {
    // TypeScript will provide autocomplete and type checking
    const record: core.Record = e.record
    const title: string = record.get("title")
    
    // Type-safe API calls
    $app.save(record)
}, "posts")
```

## Using npm packages

You can use Node.js modules via `require()`:

<Steps>
  <Step title="Initialize npm">
    ```bash theme={null}
    cd pb_hooks
    npm init -y
    ```
  </Step>

  <Step title="Install packages">
    ```bash theme={null}
    npm install lodash validator
    ```
  </Step>

  <Step title="Use in hooks">
    ```javascript pb_hooks/example.pb.js theme={null}
    const _ = require("lodash")
    const validator = require("validator")

    onRecordCreate((e) => {
        const email = e.record.get("email")
        
        if (!validator.isEmail(email)) {
            throw new BadRequestError("Invalid email address")
        }
        
        // Use lodash
        const userData = _.pick(e.record, ["name", "email", "age"])
        console.log("User data:", userData)
    }, "users")
    ```
  </Step>
</Steps>

## Best practices

<AccordionGroup>
  <Accordion title="Keep hooks focused">
    Each hook file should handle a specific domain or feature. Split complex logic into multiple files:

    ```
    pb_hooks/
    ├── auth.pb.js          # Authentication hooks
    ├── posts.pb.js         # Post-related hooks
    ├── notifications.pb.js # Email notifications
    └── routes.pb.js        # Custom routes
    ```
  </Accordion>

  <Accordion title="Error handling">
    Always handle errors appropriately:

    ```javascript theme={null}
    onRecordCreate((e) => {
        try {
            // Your logic here
            const result = $http.send({...})
            
            if (result.statusCode !== 200) {
                throw new Error("External API failed")
            }
        } catch (err) {
            console.error("Hook error:", err)
            // Decide whether to throw or continue
            throw new BadRequestError("Failed to process request")
        }
    })
    ```
  </Accordion>

  <Accordion title="Use async operations carefully">
    JavaScript hooks run synchronously. For long-running operations, log and continue:

    ```javascript theme={null}
    onRecordCreate((e) => {
        // Quick validation - blocking
        if (!e.record.get("email")) {
            throw new BadRequestError("Email required")
        }
        
        // Heavy operation - log for debugging
        console.log("Processing record:", e.record.id)
        
        // Don't block on external HTTP calls if possible
        // Consider using a queue or background job system
    })
    ```
  </Accordion>

  <Accordion title="Tag your hooks">
    Use tags to filter which collections trigger your hooks:

    ```javascript theme={null}
    // Only for posts collection
    onRecordCreate((e) => {
        // ...
    }, "posts")

    // For multiple collections
    onRecordCreate((e) => {
        // ...
    }, "posts", "articles")
    ```
  </Accordion>
</AccordionGroup>

## Debugging

### Console logging

```javascript theme={null}
console.log("Debug message")
console.error("Error message")
console.warn("Warning message")
```

### Inspect objects

```javascript theme={null}
onRecordCreate((e) => {
    console.log("Record:", JSON.stringify(e.record, null, 2))
    console.log("Original:", JSON.stringify(e.record.originalCopy(), null, 2))
})
```

### Development mode

Run PocketBase with the `--dev` flag for verbose logging:

```bash theme={null}
./pocketbase serve --dev
```

## Next steps

<CardGroup cols={2}>
  <Card title="Event hooks reference" icon="book" href="/extending/event-hooks">
    See all available event hooks
  </Card>

  <Card title="Custom routes" icon="route" href="/extending/custom-routes">
    Learn about custom API endpoints
  </Card>
</CardGroup>
