---
url: /internals/health-system.md
---
# Health system internals

Evolve's health checks run on `spatie/laravel-health`, feed the `/evolve/health` dashboard and the Oh Dear endpoint, and the `api/health-check` endpoint reports configuration and editorial activity to Fleet. The user side is on [Health Checks](/site/health-checks) and [Oh Dear](/integrations/oh-dear).

## Registration

`EvolveServiceProvider::registerHealthChecks()` registers each check behind its toggle in `health.evolve_health_check.enabled_checks` (`core/config/health.php`, merged, deliberately not publishable). Check classes live in `core/src/Health/Checks/`. Checks that belong to a module live in that module and register in its `bootModule()` with `Health::checks([...])`, which appends to the list: the email directory check (email), the Shopify API check (`shopify/src/Health`, toggle `shopify_api`) and the Scout24 API check (`scout24/src/Health`, toggle `scout24_api`). The two API checks moved out of the core in 2.21; `Racerfish\Evolve\Health\Checks\External\*` remain as deprecated subclasses with the same check names until 3.0. A disabled module never boots, so its checks do not exist even with the toggle on. Spatie refuses two checks with the same name, so a project must not register a module check a second time. Core checks run every minute (database, cache, disk space, optimized app, debug mode, environment consistency, canonical host, tracking, shadowed config, API keys, timezone, module switches, schedule, queue when a real driver is configured); the mailer check and the Shopify and Scout24 API checks run hourly and store `skipped` until due. `treat_skipped_as_failure` is `false`.

## Running

`schedule()` in the provider schedules `health:check`, `health:schedule-check-heartbeat` and, when `queueCheckEnabled()`, `health:queue-check-heartbeat` every minute, and prunes results daily after `HEALTH_KEEP_HISTORY_DAYS`. Results go to the Eloquent store (`health_check_result_history_items`, optionally on `HEALTH_DB_CONNECTION`). `evolve:health-purge` cleans by hand; the dashboard's Run button executes `health:check --no-notification`.

## Endpoints

* `/evolve/health` (route `health.dashboard`): the panel dashboard, translated and failures first since 2.13.
* `/oh-dear-health-check-results`: Spatie's Oh Dear endpoint, registered only while `OH_DEAR_HEALTH_CHECK_SECRET` is set, serving the last run's results (`HEALTH_OH_DEAR_FRESH_RESULTS` off).
* `GET /api/health-check` (`Http\Controllers\Backend\HealthCheckController`, bearer `EVOLVE_HEALTH_CHECK_TOKEN`): system information (PHP, Laravel, Evolve version, extensions, memory), project configuration, infrastructure status, environment details, the `tracking` block (type, emits, override, every public ID, secrets-set booleans, consent policy, `sources` from the settings overlay) and the `content` block (last change, who, what, `changes_30d`, `changes_90d`, `editors_90d`, last login) read from the activity log's `event` column (2.9.1). The block always ships, with nulls when there is no data.

  Since 2.21 two more blocks serve the Fleet check before modules and drivers leave the core in 3.0:

  * `modules`: one entry per module, from the keys of `evolve.modules`, the core switch `region` and every installed module provider (so package modules such as signage appear too). Each entry has `enabled` (the switch), `active` (switched on and dependencies installed, `ModuleServiceProvider::isActive()`), `source` and `missing_packages` (the suggested packages the module needs but the install lacks, `ModuleServiceProvider::missingPackages()`, empty for most modules). `source` is `env` (an `EVOLVE_MODULE_*` variable), `config` (a key in `evolve.modules`, published or shipped) or `default` (nothing switches it, the module's own default applies). `Support\ModuleSwitches` resolves the switches and records the source in `evolve.module-sources`, so it survives a config cache. A published `modules` list replaces the shipped one, so a module missing from it reports `default`.
  * `drivers`: `video` (driver, Vimeo token set), `llm` (default provider, OpenAI key set), `email` (driver, Brevo key set), `shopify` (number of stores, null without the table) and `scout24` (credentials set). Secrets only report whether they are set.

Fleet, the agency inventory, polls `api/health-check` for every instance and also probes `/mcp/evolve`.

## Notifications

With `HEALTH_NOTIFICATIONS_ENABLED`, failures go through the mail channel only, `only_on_failure`, throttled to one per hour. Since 2.18 `Health\AlertNotifiable` is bound in place of `Spatie\Health\Notifications\Notifiable` and routes to `AlertNotifiable::address()`: `evolve.mail.alert-address` (the `MailSettings` overlay, env fallback `HEALTH_WEBMASTER_ADDRESS`), then `health.notifications.mail.to`. The mailer check probes the transport on every run and sends one real test mail a day to `HEALTH_CHECK_EMAIL_RECIPIENT` (fallback the same alert address, never the from address); its meta carries `from_address`, `notification_address`, `alert_address` and a `*_source` for each. Inbox placement is not verified (2.16.1).

## Adding a check

Subclass `Spatie\Health\Checks\Check` in `core/src/Health/Checks/`, return `Result::ok()`, `warning()` or `failed()`, register it in `registerHealthChecks()` behind a new toggle, and add the toggle to `SettingsManager::BOOLEANS` so the Health tab offers it. Two rules: pass a message only to `warning()` and `failed()` (a message on `ok()` becomes a notification text), and give expensive checks a cadence (`->hourly()`) so they never block the every-minute run. Tests: `core/tests/Unit/MailerHealthCheckTest.php`, `core/tests/Feature/HealthCheckRegistrationTest.php`.
