Feature Flags
A feature flag turns a whole capability on or off. It's a runtime switch you can flip without a deploy: per environment, per tenant, eventually per user. The project uses Laravel Pennant for this.
The rule: reach for a flag when the boundary is a whole capability — a complete feature, domain, or surface area is either ON or OFF. Examples: the entire winch_branches domain (its routes, its menu groups, the internal-invoices group that depends on it). The flag is a binary capability switch.
The shape
use Domain\Core\Enums\FeatureFlag;
use Laravel\Pennant\Feature;
if (Feature::active(FeatureFlag::WinchBranches->value)) {
// capability is on
}
@feature('winch_branches')
<a href="{{ route('cp.winch_branches.index') }}">Branches</a>
@endfeature
// route file
Route::middleware('feature:winch_branches')->group(function () {
Route::resource('winch_branches', WinchBranchesController::class);
});
The flag is checked at request time (middleware), in Blade (@feature), in PHP. An inactive flag returns 404 from the middleware (configured in App\Providers\FeatureServiceProvider).
Adding a new flag
- Add a case to
Domain\Core\Enums\FeatureFlag. - Add a default in
config/features.php:'my_flag' => env('FEATURE_MY_FLAG', false), - Optionally, create a class extending
Domain\Core\Abstracts\Featurefor typedenabled()/disabled()and feature-specific helpers —WinchBranchesFeatureis the canonical example. - Routes — gate with middleware:
Route::middleware('feature:my_flag')->group(function () { Route::resource('things', ThingsController::class); }); - Blade:
@feature('my_flag') ... @endfeature - Menu — add the flag to
group_featuresorroute_featuresinconfig/cp-menu.phpso the sidebar prunes the link when the flag is off. - Operator toggles via the
FEATURE_MY_FLAGenv var.
Anti-patterns
- ❌ Adding a Pennant flag to hide a single field. Use Project Flavor — a flag has overhead (config, env, registration, middleware support) that doesn't pay off for one form field.
- ❌ Auto-deriving flag values from
ProjectFlavor. They stay independent on purpose: a saas deploy may still need to flip a flag for unrelated reasons (e.g., a new feature being soft-launched). - ❌ Forgetting menu pruning. Adding
feature:middleware without a matchinggroup_features/route_featuresentry leaves a sidebar link that 404s when clicked.
Reference files
src/Domain/Core/Enums/FeatureFlag.php— the flag enum (one case per flag).src/Domain/Core/Abstracts/Feature.php— base class for typed feature classes.config/features.php— defaults read from env vars.config/cp-menu.php—group_features/route_featuresmaps.app/Packages/Menus/CpMenu.php— menu builder that consumes the maps.App\Providers\FeatureServiceProvider— middleware 404 behaviour.
What to read next
- Project Flavor — the cosmetic / build-time counterpart, plus a comparison table.
- Deployment — how flag rollouts propagate to production.
- Domain boundaries — why a flag goes around a whole domain, not inside one.
Job Batching
When a user action fans out into many independent jobs and the UI needs a live progress bar plus a Cancel-All button — how the project wraps Laravel's native batching through the `Core\JobBatch` sub-domain, and the things you must not build.
Project Flavor
Build-time toggle for white-label differences — a single field on a form, an extra column, a different validation rule. Not for whole capabilities.