nowo-tech/maintenance-mode-bundle
nowo-tech/maintenance-mode-bundle provides a simple maintenance mode for PHP applications: toggle downtime, show a friendly holding page, and control who can access the app during maintenance windows. Lightweight, configurable, and easy to integrate.
First, the honest framing: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/maintenance.php, no Artisan command. If your day job is Laravel, the realistic ways to "leverage" this package are:
php artisan down doesn't give you.Everything below is written for case 1, with translation notes for case 2.
composer require nowo-tech/maintenance-mode-bundle
Then register the bundle in config/bundles.php and check whether a Flex recipe dropped a config file under config/packages/. If not, the authoritative source for every key is the bundle's DI extension class (DependencyInjection/*Extension.php) — read the Configuration tree before guessing at option names. The bundle targets PHP 8.2+ and Symfony ^7.4 || ^8.0, so confirm your app is inside that window first.
| Order | Look at | Why |
|---|---|---|
| 1 | The DI extension / Configuration tree | The only truthful list of config keys |
| 2 | The storage contract (interface) in src/ |
Decides whether state survives deploys and multi-node setups |
| 3 | The HTTP kernel listener/subscriber | Tells you when the short-circuit fires relative to routing, security, and the firewall |
| 4 | The panel controllers + routes | Tells you what has to be protected by access_control or a role |
| 5 | README / CHANGELOG / UPGRADING |
Release cadence and BC history — relevant given 0 dependents |
/admin and your own IP still work. If you can't get back in, you've found the lockout gotcha on day one, which is the right day to find it.Config-first, panel-second. Treat the config file as the source of truth for policy (which paths, IPs, and roles are exempt) and the panel CRUD as the source of truth for state (on/off, message, timing). Mixing the two is where teams get confused: someone toggles maintenance off in the panel while an env-specific config still exempts the wrong subnet.
Custom storage is the upgrade that matters. The built-in storage is fine for a single box; the moment you run two app servers or a blue/green deploy, file-based state diverges or gets wiped. Implement the bundle's storage interface against something shared — Redis or a small DB table — and the panel, the listener, and any CLI you write all read the same flag. This is the single highest-value integration point in the package.
Protect the panel with your existing firewall. The bundle ships CRUD routes; it does not know your user model. Put the panel path behind access_control with a role, and add it to your exclusions so authenticated operators aren't bounced to the maintenance page.
Pair it with the scheduler for windows. For "Sunday 02:00–04:00" maintenance, drive the toggle from Symfony Scheduler (or cron hitting a console command) rather than a human clicking a button. The storage abstraction makes this clean — the scheduler writes the same flag the listener reads.
Make the maintenance response fit the consumer. Browser traffic wants an HTML page; API clients want 503 with Retry-After and a JSON body. If the bundle supports a custom response/template, branch on Accept or on route prefix. Serving a Twig page to a mobile client is a support ticket generator.
Translate from Laravel deliberately. If you're building or evaluating the Laravel equivalent:
| Laravel native | Equivalent here |
|---|---|
artisan down / up |
Panel toggle or console command |
--secret bypass URL |
Excluded path or IP allowlist |
--except=/api/* |
Configurable exclusions |
--render=errors::503 |
Custom maintenance template/response |
| — | Pluggable storage (the differentiator) |
| — | Operator CRUD panel (the differentiator) |
Laravel's built-in is a file-based flag in storage/framework/. The bundle's two extras — shared storage and a UI — are exactly what a multi-node Laravel app ends up hand-rolling.
Test the exclusion matcher, not just the toggle. One functional test: hit a non-excluded route with maintenance on, assert the response status and body. One more: hit an excluded route, assert it renders normally. Those two tests catch 90% of the config mistakes people actually make.
It will not install into Laravel. Not a caveat, a hard fact — Symfony HttpKernel listeners, DI compiler passes, and Twig integration. Don't attempt to shim it; copy the design instead.
Zero stars, zero dependents, single-digit score. Treat this as unvetted. Pin an exact version, read CHANGELOG and UPGRADING before every bump, and keep a note of how you'd replace it. Also sanity-check the release date on the tag you install — if the metadata looks off, verify against the tag itself rather than trusting the listing.
Exclude the panel routes and your health checks. The most common self-inflicted outage is locking everyone — including yourself and your uptime monitor — out of the site. Exclude /health, /up, the panel path, and asset prefixes.
Exclusion matching is where bugs hide. Prefix vs regex vs glob, trailing slashes, query strings, and case sensitivity are all silently different. Verify each excluded pattern with a real request, not by reading the config.
Clear caches after changing exclusions. Symfony's compiled container and route cache will happily serve the old config. cache:clear (or the appropriate warmup) after any config edit.
Check listener priority against the security firewall. Maintenance mode firing before authentication looks correct until an admin with an expired session gets the maintenance page instead of a login redirect — or worse, gets an exception from a service that expects a user. Confirm ordering in the kernel event priority and adjust if the bundle exposes it.
CDNs and reverse proxies bypass the whole thing. If Cloudflare or Varnish serves a cached 200, the listener never runs. Send Cache-Control: no-store on maintenance responses and purge on toggle, or your "outage" will be visible only to people who've never visited before.
CLI, queue workers, and cron are not HTTP. Maintenance mode pauses web requests; it does not pause Messenger consumers or scheduled jobs. Decide explicitly whether background work should continue (usually yes) or be gated (sometimes), and implement it yourself against the same storage flag.
Keep a break-glass path. An env var, a console command, or a hard-coded IP allowlist that disables maintenance without touching the panel. When the panel is broken — and during an incident it often is — you need one command that gets the site back.
Don't hit storage on every request more than once. The listener should resolve the flag once per request (and ideally cache it in a request-scoped service). With Redis or a DB backend on a busy site, repeated lookups are a measurable tax.
Debugging checklist when maintenance mode "doesn't work": (1) is the bundle registered and the listener tagged? (2) did you clear cache after config changes? (3) is your request matching an exclusion you forgot about? (4) is a proxy serving cache? (5) is the storage backend reachable — and what does the bundle do when it isn't? Fail-open vs fail-closed on storage errors is a decision you should make explicitly, not inherit.
How can I help you explore Laravel packages today?