Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Maintenance Laravel Package

grafite/maintenance

Grafite Maintenance provides a configurable maintenance mode for Laravel applications, letting you enable downtime with allowlisted bypasses, custom views, and Artisan commands for planned deployments and upgrades.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a thin convenience layer over Laravel's own maintenance primitives — think of it as a nicer front-end for php artisan down/up, cache clearing, and the surrounding deploy chores, not a new subsystem. The value is a consistent command vocabulary and (per the description) a DB-driven maintenance state with custom pages, bypass lists, and scheduled windows. Verify the actual surface before you build a workflow on it: low adoption means the source is the spec.

Install and enumerate what you got.

composer require grafite/maintenance
php artisan list | grep -i maintenance

That second command matters more than the README. Auto-discovery will register the service provider, but there is no partial-adoption mode: once it is required, its commands are live in every environment, including production. Confirm you didn't shadow a framework built-in (see Gotchas).

Look in three places before writing code.

  1. vendor/grafite/maintenance/src/MaintenanceServiceProvider.php — which commands, under what names, and any conditional registration.
  2. vendor/grafite/maintenance/src/Commands/ — the actual behavior. This is a small package; read it in fifteen minutes.
  3. vendor/grafite/maintenance/composer.json — declared laravel/framework and php constraints, and whether it pulls other Grafite packages transitively. A declared constraint is not a tested one; check require-dev and any CI config before trusting it on PHP 8.2+/Laravel 11.

Pin it immediately. Exact version in composer.json, composer.lock committed, and ideally a fork under your org's namespace:

"require": {
    "grafite/maintenance": "2.0.1"
}

First use case: a single deploy verb. The one thing that justifies this package on day one is collapsing a Friday-afternoon ritual into one readable block — put the app down with a bypass secret, flush stale caches, migrate, bring it back up. If your deploy pipeline already does this, or you already have php artisan down --secret memorized, you are closer to "reimplement in app/Console/Commands" than "adopt." A half-day of work removes the dependency entirely.

Implementation Patterns

Wrap, don't call directly. Never let deploy scripts or runbooks invoke grafite/maintenance commands by name. Give yourself a seam:

// app/Console/Commands/DeployRefresh.php
class DeployRefresh extends Command
{
    protected $signature = 'app:deploy-refresh {--secret= : Bypass token for stakeholders}';

    public function handle(): int
    {
        Artisan::call('down', ['--secret' => $this->option('secret'), '--render' => 'errors.503']);

        try {
            Artisan::call('migrate', ['--force' => true]);
            Artisan::call('config:clear');
            Artisan::call('view:clear');
            Artisan::call('route:clear');
            // swap in the package command here if it genuinely adds behavior
        } finally {
            Artisan::call('up');
        }

        return self::SUCCESS;
    }
}

The try/finally is the whole point: a failed migration must not leave the fleet down. Whatever the package's up-equivalent is, own that wrapper so you can swap the implementation later without touching runbooks.

Maintenance mode as a scheduled window, not an emergency. If the package supports scheduled downtime, express it in app/Console/Kernel.php (or routes/console.php in Laravel 11) with concurrency guards, so the window is reviewable in a PR rather than executed ad hoc:

$schedule->command('maintenance:enable', ['--message' => 'Planned upgrade 02:00–02:30 UTC'])
    ->dailyAt('02:00')
    ->onOneServer()
    ->when(fn () => now()->isWednesday());

$schedule->command('maintenance:disable')
    ->dailyAt('02:30')
    ->onOneServer();

onOneServer() matters. Without it, every app server tries to flip state and write the same record, and the loser's error is the only trace you get.

Bypass lists belong in config, not in the database UI. For IP allowlisting and user bypass, prefer a config file with env overrides so the list is diffable and auditable:

// config/maintenance.php
'allowed_ips' => array_filter(explode(',', env('MAINTENANCE_ALLOWED_IPS', ''))),
'allowed_user_ids' => array_map('intval', explode(',', env('MAINTENANCE_ALLOWED_USERS', ''))),

Then have a single middleware consult it. If the package ships its own middleware, still verify it runs before your auth middleware if you're bypassing by user — you cannot resolve Auth::user() from a session that hasn't been loaded yet. For user-based bypass, php artisan down --secret=... plus a shared cookie is usually simpler and framework-native.

Custom 503 rendering is a deploy artifact. Keep the view at resources/views/errors/503.blade.php (framework default) or pass --render explicitly. Anything the maintenance page needs — logo, status link — must be inlined or served from a CDN, because your asset pipeline and database are, by definition, unavailable. Don't reference Vite::asset().

Programmatic toggling for tests and local dev. In feature or Dusk tests, don't shell out:

$this->artisan('down')->assertExitCode(0);
$this->assertTrue(app()->isDownForMaintenance());
// ...
$this->artisan('up')->assertExitCode(0);

Gate any destructive cache:clear-style commands behind a confirmation or a --force flag in your wrapper, and run them against a disposable environment in CI before they reach a runbook.

Gotchas and Tips

Silent command-name collisions. Auto-discovery registers the provider and the winning definition for a duplicated name is not obvious. If the package aliases optimize:clear or cache:clear, you may get framework behavior while believing you're getting package behavior — or vice versa. Prove it: php artisan list | grep -i <name> after install, and again after each Laravel major upgrade. A command that is registered but does nothing after an upgrade erodes runbook trust faster than a missing one.

config:cache will bite you in exactly one environment. The package's DB-driven state is fine; your env-driven bypass list is not, if you cached config in staging and forgot in prod. Anything read from env() must go through a config file — direct env() calls return null once config is cached. Test the cached path (php artisan config:cache) in CI, not just locally.

Cache clears cause stampedes. Wholesale cache:clear on a high-traffic app drops the origin database's shield and every request misses simultaneously. Prefer targeted invalidation, schedule clears during the maintenance window itself, and never run them on a schedule that overlaps with traffic peaks. This is the one "convenience" command that can cause an outage in production.

DB-backed maintenance state vs. the down file. If the package stores state in the database, you now have a chicken-and-egg problem: the app needs to hit the DB to know it's down, and the maintenance page may need the DB to render a custom message. Confirm the check runs early enough in the stack and that the 503 page doesn't depend on Eloquent. The native down file has no such coupling — that's a feature, and it's why Laravel still ships it.

Destructive commands have no dry run. Storage pruning and cache clearing don't offer a preview in most packages. Wrap them, log what they touched, and test against production-shaped data — storage layouts and cache backends differ meaningfully between local (file cache, storage/app) and prod (Redis, shared NFS, S3 mounts).

Fleet partial failure. If a command runs across N hosts and fails on host 3, you're left with inconsistent cache state and no rollback. Design for idempotency: any maintenance command should be safe to re-run from the top. onOneServer() covers scheduled commands; deploy-hook commands need an equivalent lock.

Supply-chain reality check. Zero stars, zero dependents means no CVE path, no compatibility pressure, and no maintainer SLA. Assume you will patch it yourself after the first framework major that breaks it. Keep it out of backups, PII retention, and multi-tenant isolation paths entirely — those need spatie/laravel-backup or native tooling with maintained owners. For everything else, the fork-and-own cost is hours, which makes carrying an unmaintained dependency hard to defend in review.

Testing hooks. $this->artisan('down') in tests leaves global state behind if an assertion fails mid-test. Use tearDown() or a finally to guarantee up, or your suite will produce baffling 503s on the next test in the class.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment