PHPackages                             gavtaylor/laravel-health-route - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. gavtaylor/laravel-health-route

ActiveLibrary

gavtaylor/laravel-health-route
==============================

A drop-in replacement for Laravel's built-in health route, with a customisable HTML view, a richer JSON contract, opt-in structured checks, and composable access control.

v0.2.0(today)111↑2900%MITPHPPHP ^8.3CI passing

Since Aug 28Pushed todayCompare

[ Source](https://github.com/gavtaylor/laravel-health-route)[ Packagist](https://packagist.org/packages/gavtaylor/laravel-health-route)[ Docs](https://github.com/gavtaylor/laravel-health-route)[ RSS](/packages/gavtaylor-laravel-health-route/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (15)Versions (3)Used By (0)

Health Route Extension for Laravel
==================================

[](#health-route-extension-for-laravel)

[![Latest Version on Packagist](https://camo.githubusercontent.com/ac02645e23d8b3910abb41022525c598296a047ca7a8388aa7f763b91cac3fd6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6761767461796c6f722f6c61726176656c2d6865616c74682d726f7574652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/gavtaylor/laravel-health-route)[![tests](https://github.com/gavtaylor/laravel-health-route/actions/workflows/tests.yml/badge.svg)](https://github.com/gavtaylor/laravel-health-route/actions/workflows/tests.yml)[![Total Downloads](https://camo.githubusercontent.com/793ac3ba4e040e757fc39592c0000374e9e8671e5529cc1e4bcf3259777ab3ee/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6761767461796c6f722f6c61726176656c2d6865616c74682d726f7574652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/gavtaylor/laravel-health-route)

A drop-in replacement for Laravel's built-in health route, with a customisable HTML view, a richer JSON contract, opt-in structured checks, and composable access control.

Laravel core ships a built-in health check route (`health:` in `withRouting()`): served at a configurable URI (default `/up`), it dispatches `Illuminate\Foundation\Events\DiagnosingHealth`, and returns `200`/`500` with `{"status": "up"|"down"}` for JSON clients. Its HTML page is deliberately not customisable.

This package **is that same route** — same event, same failure contract, same default path — with a customisable HTML view and a library of opt-in checks layered on top. Remove the package and add `health: '/up'` back to `withRouting()`, and any client that only reads `status` sees no change in behaviour.

Installation
------------

[](#installation)

```
composer require gavtaylor/laravel-health-route
```

The package auto-registers itself and serves `/up` immediately - no other setup is required. If your app still passes `health: '/up'` to `withRouting()` in `bootstrap/app.php`, remove it; this package logs a boot-time warning if it detects another route already registered at the same path. The route is named `health-route` (`route('health-route')`).

The HTML view
-------------

[](#the-html-view)

Override the default view with zero config change:

```
php artisan vendor:publish --tag=health-route-views
```

This copies the view into `resources/views/vendor/health-route/default.blade.php`, where Laravel's own view resolution already looks before falling back to the package's copy. Edit it directly - nothing else needs to change.

The view receives:

VariableTypeDescription`$exception``Throwable|null`Set when a `DiagnosingHealth` listener threw (core's own failure mode)`$down``bool`Whether the overall response is reporting "down"`$checks``list`Every configured check's result (empty if none are configured)This is a public contract: once you've customised the view, treat changes to these variables as breaking changes.

Configuration
-------------

[](#configuration)

```
php artisan vendor:publish --tag=health-route-config
```

See the generated `config/health-route.php` for every option, documented inline. Highlights below.

Checks
------

[](#checks)

Register named checks that each independently report `up`, `degraded`, or `down`, with an optional message and structured context:

```
// config/health-route.php
'checks' => [
    \GavTaylor\HealthRoute\Checks\DatabaseConnectionCheck::class,
    \GavTaylor\HealthRoute\Checks\CacheReadWriteCheck::class,
],
```

**A `degraded` check never fails the HTTP response** - only a `down` check does, using the configured `problem_status_code` (default `503`). This is the point of checks beyond a single up/down boolean: surface a real problem without paging on-call for something that isn't urgent.

The JSON payload gains a `checks` array only when at least one check is configured, so the default response stays byte-for-byte identical to core. JSON is returned when the client asks for it (`Accept: application/json`, or `$this->getJson()` in tests), matching `$request->expectsJson()` in Laravel core. Anything else, including a missing or `*/*` Accept header, gets the HTML view.

```
{
    "status": "down",
    "checks": [
        {"name": "database", "status": "down", "message": "Could not connect to the database."}
    ]
}
```

No check leaks exception detail (messages, file paths, stack traces) into the response - the endpoint is public by default, and every response body should be treated as something an unauthenticated caller can read. Full detail always goes to your logger via `report()`, never to the HTTP response.

A check's `message` can be `null` (e.g. an `up` result with nothing to add) - the JSON payload keeps it as `null`, while the default HTML view renders a `-` so the cell doesn't look broken.

Checks run and appear in the JSON payload in the order they're listed in `checks` above. The default HTML view instead sorts its table alphabetically by name, for easier scanning - this is presentational only and doesn't affect the JSON order or check execution order.

### Bundled checks

[](#bundled-checks)

All opt-in, none run unless listed in `checks` above:

- `DatabaseConnectionCheck` - runs a trivial query against a database connection
- `CacheReadWriteCheck` - writes then reads back a probe value from a cache store
- `RedisCheck` - pings a Redis connection
- `DiskSpaceCheck` - free disk space against configurable degraded/down thresholds
- `StorageWritableCheck` - confirms `storage/framework/{cache,sessions,views}` are actually writable, by writing and removing a probe file (not just inspecting permission bits)
- `LogWritableCheck` - same writability probe, against the log directory
- `EnvironmentCheck` - a configurable list of required environment variables are present, and that `app.debug` isn't accidentally enabled outside a configured "safe" environment (default `local`, `testing`)
- `OutboundHttpCheck` - probes a configured URL (no redirects; connect timeout capped at 3s)
- `PendingMigrationsCheck` - degrades when migrations haven't been run
- `SchedulerLivenessCheck` - degrades/downs based on a heartbeat timestamp (see below)
- `DependencyAdvisoryCheck` - wraps `composer audit`, cached far longer than other checks since it's expensive (requires `composer` on PATH)
- `CrossServiceCheck` - probes another service's own health-style endpoint (`up` / `degraded` / `down`)

Each check's tunables (connection names, thresholds, URLs) live under `checks_config` in the config file.

Write your own check by implementing `GavTaylor\HealthRoute\Checks\Contracts\Check`. Never put exception messages, file paths, or stack traces in the result - the endpoint is public by default:

```
use GavTaylor\HealthRoute\Checks\CheckResult;
use GavTaylor\HealthRoute\Checks\Contracts\Check;

final class QueueDepthCheck implements Check
{
    public function name(): string
    {
        return 'queue';
    }

    public function run(): CheckResult
    {
        if (/* the queue is too deep */) {
            return CheckResult::degraded($this->name(), 'Queue depth is above the warning threshold.');
        }

        return CheckResult::up($this->name());
    }
}
```

#### Scheduler heartbeat

[](#scheduler-heartbeat)

`SchedulerLivenessCheck` needs something to write a heartbeat. Set `checks_config.scheduler.register_heartbeat` to `true` and this package registers an `everyMinute()` scheduled task itself - or write to the same cache key yourself (`SchedulerLivenessCheck::HEARTBEAT_CACHE_KEY`) from your own scheduled command if you'd rather not use the bundled one. No scheduled job is *required* for any other feature in this package.

Access control
--------------

[](#access-control)

The endpoint is public by default, matching core. Each method below is independently configurable and composable - if more than one is enabled, passing **any single one** is enough to see the full response:

```
// config/health-route.php
'access' => [
    'bypass_when_local' => false,
    'basic_auth' => ['username' => env('HEALTH_ROUTE_BASIC_AUTH_USERNAME'), 'password' => env('HEALTH_ROUTE_BASIC_AUTH_PASSWORD')],
    'token' => ['header' => 'X-Health-Token', 'value' => env('HEALTH_ROUTE_TOKEN_VALUE')],
    'allowed_ips' => ['10.0.0.0/8', '203.0.113.5'],       // IPv4/IPv6, CIDR supported
    'allowed_hostnames' => ['monitor.example.ddns.net'],   // for callers on a dynamic IP
],
```

- **Local-development bypass** - explicit, off by default, checked against `app()->environment('local')` only, never the request's IP (which can be spoofed or misreported by a misconfigured proxy).
- **Basic auth** - for monitoring tooling that can only authenticate with a username/password.
- **Shared-secret header** - cheaper to rotate than credentials, never appears in a URL.
- **Static IP/CIDR allowlist** - for infrastructure with fixed addresses.
- **Dynamic hostname allowlist** - for a caller behind a DDNS record. Resolved via DNS and cached for the record's own TTL, so a lookup only happens once per TTL window. A failed lookup is cached briefly too, so a DNS outage doesn't turn every request into a slow one - PHP has no reliable built-in DNS timeout, so this is an accepted limitation: at most one unlucky request per outage pays the cost of a slow lookup.

All credential/token comparisons are timing-safe. An unconfigured method never accidentally authenticates - empty config never matches empty or absent credentials, and an empty allowlist never matches any IP.

A caller that fails every configured method still receives the real HTTP status code, with no response body - an unauthenticated monitor can learn "up or down," nothing more.

**Configured checks still run** before the body is withheld, so the status code is accurate. Treat outbound HTTP, cross-service, and `composer audit` checks as side effects on every request to the path - not only authenticated ones.

IP and hostname allowlists use `$request->ip()`. Configure [trusted proxies](https://laravel.com/docs/requests#configuring-trusted-proxies) correctly; a misconfigured proxy can make allowlists too wide or too narrow.

Status header for other routes
------------------------------

[](#status-header-for-other-routes)

Attach a lightweight check-status header to any other route:

```
Route::middleware('health-status')->group(function () {
    // ...
});
```

Enable it via `status_header.enabled` in the config file. It reads through the same short-lived cache the main endpoint uses (`checks_cache_seconds`), so attaching it to many routes doesn't force the full check suite to re-run on every request.

Static liveness file
--------------------

[](#static-liveness-file)

For a cheaper first-line check than booting the framework at all:

```
php artisan vendor:publish --tag=health-route-static
```

This publishes a static `public/ping` file - hit `/ping` and the web server replies `pong` directly, without booting Laravel at all. Never a registered framework route, never access-controlled by this package, and never written automatically at request or boot time. Use it as a cheap liveness probe and keep `/up` for readiness.

Customise the filename via `static_filename` if you like, but don't set it to match `path` (for example `up` vs `/up`): most web servers would serve the static file and silently bypass the dynamic route, including its access control. The package logs a critical message at boot time if it detects that - it's worth watching for in your logs, since nothing stops the app from booting.

Re-running the publish command never overwrites a file you've already customised (standard `vendor:publish` behaviour) - use `--force` if you want to reset it.

Testing
-------

[](#testing)

See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup/lint/test workflow.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Issues and pull requests are welcome. As a native-focused, drop-in package, code changes are held to [Laravel's own coding standards](https://laravel.com/framework/docs/contributions#coding-style) - see [CONTRIBUTING.md](.github/CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](.github/SECURITY.md) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Gavin Taylor](https://github.com/gavtaylor)
- [All Contributors](../../contributors)

This package's design is informed by prior art: an MIT-licensed Laravel health-check package the author previously contributed to. The implementation here is entirely new, built from a fresh set of functional requirements rather than ported from that or any other package.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/408490?v=4)[Gavin Taylor](/maintainers/gavtaylor)[@gavtaylor](https://github.com/gavtaylor)

---

Top Contributors

[![gavtaylor](https://avatars.githubusercontent.com/u/408490?v=4)](https://github.com/gavtaylor "gavtaylor (9 commits)")

---

Tags

laravelhealthhealth checkuptimegavtaylorlaravel-health-route

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/gavtaylor-laravel-health-route/health.svg)

```
[![Health](https://phpackages.com/badges/gavtaylor-laravel-health-route/health.svg)](https://phpackages.com/packages/gavtaylor-laravel-health-route)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

80427.1M252](/packages/laravel-mcp)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k31.8M163](/packages/laravel-cashier)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[illuminate/queue

The Illuminate Queue package.

20433.0M1.9k](/packages/illuminate-queue)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M343](/packages/laravel-ai)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
