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

# Reserved Collections

> PocketBase system collections used by pb-ext for analytics, sessions, and job logs

# Reserved Collections

pb-ext creates the following PocketBase system collections automatically on startup. **Do not create collections with these names in your own code.**

<Warning>
  Creating collections with these reserved names will cause conflicts and may break pb-ext functionality.
</Warning>

## Collections Table

| Collection            | Purpose                                                                                         |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| `_analytics`          | Daily aggregated page view counters (one row per path/date/device/browser). Retention: 90 days. |
| `_analytics_sessions` | Ring buffer of the 50 most recent visits for the Recent Activity display. No PII stored.        |
| `_job_logs`           | Cron job execution logs (start time, end time, duration, status, output). Retention: 72 hours.  |

## \_analytics

**Purpose**: Stores daily aggregated visitor analytics data.

**Type**: System collection (hidden from PocketBase Collections UI)

**Retention**: Automatically purged after **90 days**

### Schema

| Field             | Type    | Description                                                  |
| ----------------- | ------- | ------------------------------------------------------------ |
| `path`            | string  | Page path (e.g., `/api/v1/todos`)                            |
| `date`            | date    | Aggregation date (day-level)                                 |
| `device`          | string  | Device category: `desktop`, `mobile`, `tablet`, or `unknown` |
| `browser`         | string  | Browser family: `Chrome`, `Firefox`, `Safari`, etc.          |
| `views`           | integer | Total page views for this combination                        |
| `unique_visitors` | integer | Estimated unique visitors (deduplicated by session)          |

### GDPR Compliance

**No PII stored**:

* No IP addresses
* No user agents
* No visitor IDs
* No session tokens

Only aggregated counts by device/browser category.

### Example Query

```go theme={null}
records, err := app.FindRecordsByFilter(
    "_analytics",
    "date >= {:today}",
    "-views",
    100,
    0,
    dbx.Params{"today": time.Now().Format("2006-01-02")},
)
```

## \_analytics\_sessions

**Purpose**: Ring buffer of the 50 most recent visitor sessions for the "Recent Activity" dashboard widget.

**Type**: System collection (hidden from PocketBase Collections UI)

**Retention**: Only the **50 most recent** records are kept (ring buffer)

### Schema

| Field          | Type     | Description                               |
| -------------- | -------- | ----------------------------------------- |
| `timestamp`    | datetime | Visit timestamp                           |
| `path`         | string   | Visited page path                         |
| `device`       | string   | Device category                           |
| `browser`      | string   | Browser family                            |
| `referrer`     | string   | Referrer URL (sanitized, no query params) |
| `utm_source`   | string   | UTM source parameter                      |
| `utm_medium`   | string   | UTM medium parameter                      |
| `utm_campaign` | string   | UTM campaign parameter                    |

### GDPR Compliance

**No PII stored**:

* No IP addresses
* No user agents
* No cookies
* No fingerprinting

### Ring Buffer Behavior

When the 51st record is inserted:

1. Oldest record is deleted
2. New record is inserted
3. Collection always contains ≤ 50 records

## \_job\_logs

**Purpose**: Stores cron job execution logs.

**Type**: System collection (hidden from PocketBase Collections UI)

**Retention**: Automatically purged after **72 hours**

### Schema

| Field         | Type     | Description                         |
| ------------- | -------- | ----------------------------------- |
| `job_id`      | string   | Unique job identifier               |
| `job_name`    | string   | Human-readable job name             |
| `start_time`  | datetime | Execution start timestamp           |
| `end_time`    | datetime | Execution end timestamp             |
| `duration_ms` | integer  | Execution duration in milliseconds  |
| `status`      | string   | `success`, `failed`, or `running`   |
| `output`      | text     | Structured JSON log output          |
| `error`       | text     | Error message if status is `failed` |
| `progress`    | integer  | Progress percentage (0-100)         |
| `statistics`  | json     | Custom job statistics (optional)    |

### Example Query

```go theme={null}
// Get logs for specific job
records, err := app.FindRecordsByFilter(
    "_job_logs",
    "job_id = {:jobId}",
    "-start_time",
    50,
    0,
    dbx.Params{"jobId": "myCustomJob"},
)

// Get failed jobs from last 24 hours
records, err := app.FindRecordsByFilter(
    "_job_logs",
    "status = 'failed' && start_time >= {:since}",
    "-start_time",
    100,
    0,
    dbx.Params{"since": time.Now().Add(-24 * time.Hour)},
)
```

### System Jobs

pb-ext registers these system jobs automatically:

| Job ID                    | Schedule                     | Description                                   |
| ------------------------- | ---------------------------- | --------------------------------------------- |
| `__pbExtLogClean__`       | `0 0 * * *` (daily midnight) | Purge `_job_logs` records older than 72 hours |
| `__pbExtAnalyticsClean__` | `0 3 * * *` (daily 3 AM)     | Purge `_analytics` rows older than 90 days    |

They appear in the dashboard with the "System" badge.

## Auto-Migration on Upgrade

**Schema notes**:

* All three collections are system collections (hidden from the PocketBase Collections UI)
* On upgrade from an old pb-ext version, incompatible schemas are automatically migrated at startup
* **No manual steps required**

### Migration Process

1. pb-ext checks if collection exists
2. If exists, validates schema matches expected
3. If schema mismatch, runs migration
4. If doesn't exist, creates with correct schema

### Logs

Migration logs appear in server output:

```
[INFO] pb-ext: migrating _analytics collection schema
[INFO] pb-ext: _job_logs collection created
[INFO] pb-ext: all system collections ready
```

## Accessing Collections Programmatically

### Read Analytics Data

```go theme={null}
package main

import (
    "github.com/pocketbase/pocketbase/core"
)

func getTopPages(app core.App) ([]*core.Record, error) {
    return app.FindRecordsByFilter(
        "_analytics",
        "date >= {:week_ago}",
        "-views",
        10,
        0,
        dbx.Params{
            "week_ago": time.Now().AddDate(0, 0, -7).Format("2006-01-02"),
        },
    )
}
```

### Read Job Logs

```go theme={null}
func getJobHistory(app core.App, jobID string) ([]*core.Record, error) {
    return app.FindRecordsByFilter(
        "_job_logs",
        "job_id = {:id}",
        "-start_time",
        20,
        0,
        dbx.Params{"id": jobID},
    )
}
```

<Warning>
  Do not manually create, update, or delete records in these collections. Use pb-ext's provided APIs instead.
</Warning>

## Dashboard Integration

pb-ext's dashboard at `/_/_` visualizes data from these collections:

**Analytics Tab**:

* Page view charts (from `_analytics`)
* Device breakdown
* Browser distribution
* Recent activity feed (from `_analytics_sessions`)

**Jobs Tab**:

* Registered cron jobs
* Execution history (from `_job_logs`)
* Job status monitoring
* Manual job triggers

## Customizing Retention

Retention is controlled by the system cleanup jobs. To customize:

### Analytics Retention (default: 90 days)

```go theme={null}
// Unregister default cleanup job
server.GetJobManager().RemoveJob("__pbExtAnalyticsClean__")

// Register custom cleanup with different retention
server.GetJobManager().RegisterJob(
    "customAnalyticsClean",
    "Custom Analytics Cleanup",
    "Clean analytics older than 180 days",
    "0 3 * * *", // daily at 3 AM
    func(logger *server.JobExecutionLogger) {
        logger.Start()
        cutoff := time.Now().AddDate(0, 0, -180) // 180 days
        _, err := app.Delete("_analytics", "date < {:cutoff}", dbx.Params{"cutoff": cutoff})
        if err != nil {
            logger.Fail(err.Error())
        } else {
            logger.Success("Analytics cleaned")
        }
    },
)
```

### Job Logs Retention (default: 72 hours)

```go theme={null}
server.GetJobManager().RemoveJob("__pbExtLogClean__")

server.GetJobManager().RegisterJob(
    "customLogClean",
    "Custom Log Cleanup",
    "Clean job logs older than 7 days",
    "0 0 * * *",
    func(logger *server.JobExecutionLogger) {
        logger.Start()
        cutoff := time.Now().AddDate(0, 0, -7) // 7 days
        _, err := app.Delete("_job_logs", "start_time < {:cutoff}", dbx.Params{"cutoff": cutoff})
        if err != nil {
            logger.Fail(err.Error())
        } else {
            logger.Success("Logs cleaned")
        }
    },
)
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Don't Create These Collections" icon="ban">
    Never manually create collections with reserved names. Let pb-ext handle initialization.
  </Card>

  <Card title="Read-Only Access" icon="eye">
    Treat these as read-only from your application code. Write operations should go through pb-ext APIs.
  </Card>

  <Card title="Monitor Disk Usage" icon="database">
    If analytics volume is high, consider shorter retention periods or archival strategies.
  </Card>

  <Card title="Backup Carefully" icon="floppy-disk">
    These collections change frequently. Exclude from backups or use incremental strategies.
  </Card>
</CardGroup>

## Further Reading

* [Reserved Routes](/advanced/reserved-routes) - Protected pb-ext routes
* [Middleware](/advanced/middleware) - Analytics middleware integration
