# Installing Sailfish

> Grant the database user what it needs, require the package, migrate and narrow the gate. The first snapshot is taken within five minutes.

Source: https://boring-observability.dev/sailfish/docs/installation
Section: Getting started — Sailfish documentation
Updated: 2026-10-02

---

Sailfish is a Laravel package with its own routes, views and assets. Installing it takes a Composer registry, a migration and one service provider, and the scheduler you already run does the rest. It reads performance_schema over one of your application's database connections and stores its history in tables next to your own.

## Requirements

- PHP 8.2 or newer
- Laravel 12 or 13
- MySQL 8.0
- The Laravel scheduler running on at least one server (`php artisan schedule:run` every minute)
- A database user that can `SELECT` from `performance_schema`

## Database grants

Sailfish uses one connection, both to read performance_schema and to store its `sailfish_*` tables. Its user needs `SELECT` on `performance_schema`. An application user granted only its own schema does not have that, and `sailfish:collect` names the grant it is missing rather than recording nothing.

```sql
GRANT SELECT ON performance_schema.* TO 'app'@'%';

-- Optional: index sizes
GRANT SELECT ON mysql.innodb_index_stats TO 'app'@'%';

-- Optional: InnoDB's history list length
GRANT PROCESS ON *.* TO 'app'@'%';
```

The two optional grants each add one figure. Without `mysql.innodb_index_stats` the dashboard shows no index sizes, and without `PROCESS` the history list chart and its [event](https://boring-observability.dev/sailfish/docs/events#the-server) stay empty. A user with `ALL` on `*.*` already has all three.

If your application's user should not have these, point `SAILFISH_DB_CONNECTION` at a second connection to the same server whose user does. See [configuration](https://boring-observability.dev/sailfish/docs/configuration).

## 1. Authenticate Composer

Sailfish is distributed through a private Composer registry. Your license key is issued through [Anystack](https://anystack.sh/) at checkout. Run this once on every machine that installs the package, including CI and build servers:

```bash
composer config --global --auth http-basic.sailfish.composer.sh \
  you@example.com \
  YOUR-LICENSE-KEY
```

The username is the email address the license belongs to and the key is the password. On servers, supply the same pair through `COMPOSER_AUTH` instead of committing an `auth.json`:

```bash
export COMPOSER_AUTH='{"http-basic":{"sailfish.composer.sh":{"username":"you@example.com","password":"YOUR-LICENSE-KEY"}}}'
```

## 2. Require the package and migrate

Add the registry to your application's `composer.json`:

```json
{
    "repositories": [
        { "type": "composer", "url": "https://sailfish.composer.sh" }
    ]
}
```

Then require Sailfish and run the migrations:

```bash
composer require boring-o11y/my-sailfish
php artisan migrate
```

Package discovery registers Sailfish's own service provider, which adds the commands, the schedule and the dashboard routes. The migrations create the `sailfish_*` tables described in [what gets recorded](https://boring-observability.dev/sailfish/docs/what-gets-recorded).

## 3. Let the scheduler run

Sailfish puts its commands on Laravel's scheduler for you:

| Command | Scheduled | What it does |
| --- | --- | --- |
| `sailfish:collect` | Every five minutes, `withoutOverlapping` and `onOneServer` | Reads performance_schema, stores what moved since the last run, then runs `sailfish:detect` |
| `sailfish:daily-digest` | Daily at 09:00 | Sends the events that go out in the [daily digest](https://boring-observability.dev/sailfish/docs/notifications#digest) |
| `sailfish:rollup` | Daily at 03:00 | Merges old snapshots into hourly and daily ones (see [retention](https://boring-observability.dev/sailfish/docs/retention)) |

The first collection only records a baseline, because counters running since the server booted describe no particular interval. Samples start with the second one, five minutes later. Set `SAILFISH_SCHEDULE_ENABLED=false` to register the commands yourself.

## 4. Decide who sees the dashboard

The dashboard is served at `/sailfish`. In the `local` environment anyone can open it. Everywhere else nobody gets in until you say who can. Publish the provider:

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

Register it in `bootstrap/providers.php`, then narrow its gates:

```php
// app/Providers/SailfishServiceProvider.php
protected function gate(): void
{
    Gate::define('viewSailfish', fn ($user) => $user->isAdmin());
    Gate::define('viewSailfishQueryText', fn ($user) => $user->isDba());
}
```

`viewSailfish` opens the dashboard. `viewSailfishQueryText` decides who sees running statements on the Activity page as they were sent, with their values. Everyone else sees them with each literal replaced by `?`, because those values can be email addresses and tokens.

## 5. Check it

`php artisan about` gets a Sailfish section with the connection, the schemas watched, the last snapshot and the retention. To take a snapshot straight away rather than wait for the scheduler:

```bash
php artisan sailfish:collect
```

> **Next**
>
> Sailfish sends nothing until you give it somewhere to send. Set up [notifications](https://boring-observability.dev/sailfish/docs/notifications) to hear about the [events](https://boring-observability.dev/sailfish/docs/events) it finds.


## Common questions

### Why does sailfish:collect say it is missing a grant?

The connection's user cannot SELECT from performance_schema. An application user granted only its own schema cannot, and Sailfish names the grant it needs rather than recording nothing. Grant SELECT on performance_schema.* to that user, or point SAILFISH_DB_CONNECTION at a connection whose user has it.

### Why is the Sailfish dashboard empty after installing?

The first collection only records a baseline, because counters that have been running since the server booted do not describe any interval. Samples start with the second collection, five minutes later. If nothing appears after that, check that php artisan schedule:run is running every minute.

### Why does the Sailfish dashboard return 403 in production?

Outside the local environment the dashboard asks the viewSailfish gate, and nobody gets in until you define it. Publish the provider with php artisan vendor:publish --tag=sailfish-provider, register it, and narrow the gate to the users who should see it.
