# Configuration reference

> Every environment variable, its default, and the parts of config/sailfish.php that have none.

Source: https://boring-observability.dev/sailfish/docs/configuration
Section: Configuration — Sailfish documentation
Updated: 2026-10-02

---

Sailfish works with no configuration on an application that watches its own database. Everything it reads is set by environment variables with the defaults below. Detector thresholds, notification types and retention have no environment variable; change them in the published config:

```bash
php artisan vendor:publish --tag=sailfish-config
```

## Environment variables

| Variable | Default | What it does |
| --- | --- | --- |
| `SAILFISH_ENABLED` | `true` | Master switch. When false Sailfish registers no commands, no schedule and no dashboard. Its migrations still load. |
| `SAILFISH_DB_CONNECTION` | app default | Read performance_schema from here, and store the `sailfish_*` tables here |
| `SAILFISH_SCHEMAS` | the connection's database | Comma-separated databases whose tables, indexes and statements are recorded |
| `SAILFISH_DIGEST_LIMIT` | `1000` | Most statement digests read per collection |
| `SAILFISH_SCHEDULE_ENABLED` | `true` | Put the commands on the scheduler. False to schedule them yourself. |
| `SAILFISH_PATH` | `sailfish` | Where the dashboard is served |
| `SAILFISH_DOMAIN` | none | Serve the dashboard on this domain only |
| `SAILFISH_NOTIFY_CHANNELS` | none | Comma-separated: `mail`, `slack`, `webhook` |
| `SAILFISH_NOTIFY_MAIL` | none | Comma-separated addresses |
| `SAILFISH_SLACK_WEBHOOK_URL` | none | An incoming webhook, or a `#channel` to post to through the bot token |
| `SAILFISH_WEBHOOK_URL` | none | Where the `webhook` channel POSTs JSON |
| `SAILFISH_NOTIFY_REPEAT_MINUTES` | `360` | Send an immediate event again while it stays open this long. `0` never repeats. |
| `SAILFISH_DAILY_DIGEST_AT` | `09:00` | When the daily digest goes out |
| `SAILFISH_ROLLUP_AT` | `03:00` | When `sailfish:rollup` runs |

## Connection and schemas

Sailfish uses one connection for both jobs. Leaving `SAILFISH_DB_CONNECTION` empty uses the application's default, and leaving `SAILFISH_SCHEMAS` empty watches the database that connection points at, which is the right answer for an application watching itself.

To keep the performance_schema grant off your application's user, define a second connection to the same server with a user that has it, and name that one:

```bash
SAILFISH_DB_CONNECTION=mysql_sailfish
SAILFISH_SCHEMAS=app,billing
```

performance_schema is server-wide, so a connection on the same server can watch any schema on it. The `sailfish_*` tables are written to the database that connection points at.

## Statement digests

Each collection reads only the digests that ran since the previous one, most recently run first, up to `SAILFISH_DIGEST_LIMIT`. The cap matters only after a long gap. A digest it skips is not lost: its counters are cumulative, so the next collection that reads it records everything it missed. If your digests are empty, see [statement digests](https://boring-observability.dev/sailfish/docs/statement-digests).

## Dashboard

```php
'domain' => env('SAILFISH_DOMAIN'),
'path' => env('SAILFISH_PATH', 'sailfish'),
'middleware' => ['web'],

'dashboard' => [
    'windows' => [1, 7, 30, 90],   // days the charts offer
    'default_window' => 7,
    'per_page' => 100,
],
```

The dashboard runs through `middleware` and then Sailfish's own check against the `viewSailfish` gate. `default_window` should be one of `windows`.

## Detectors

Every detector has a key under `detectors` with an `enabled` flag and its thresholds. To turn one off, or to make the table-scan surge less sensitive:

```php
'detectors' => [
    'scan_surge' => [
        'enabled' => true,
        'history_days' => 7,
        'ratio' => 3,
        'min_rows_per_hour' => 100_000,
    ],
    'no_primary_key' => [
        'enabled' => false,
    ],
    // ...
],
```

[Events](https://boring-observability.dev/sailfish/docs/events) lists every detector with its key and the thresholds it takes.

## Notifications

`notifications.types` gives each event type a severity (`critical`, `warning` or `info`) and says whether it is sent `immediate`ly, in the `digest`, or `none` at all. `notifications.routes` picks channels per severity. Both are covered in [notifications](https://boring-observability.dev/sailfish/docs/notifications).

## Retention

```php
'retention' => [
    'raw_days' => 7,
    'hourly_days' => 90,
    'digest_days' => 90,
],
```

`null` keeps that granularity, or those digests, forever. See [retention and rollups](https://boring-observability.dev/sailfish/docs/retention).
