laravel-feature-flags maintained by chemaclass
laravel-feature-flags
A full feature-flag toolkit for Laravel that needs nothing but your database. Simple on/off toggles when that's all you want — attribute targeting, percentage rollouts, A/B variants, per-environment values, and a Blade admin UI when you need more.
Drop it in any Laravel app. No external service, no vendor lock-in, no infra to run.
▶ Live demo & feature tour — how it works, the admin UI, and every feature at a glance.
if (FeatureFlag::isEnabled('new-dashboard')) {
// ship it
}
Install
composer require chemaclass/laravel-feature-flags
php artisan vendor:publish --tag=feature-flags
php artisan migrate
Auto-registers itself. Publishes the config, migration, admin view, and admin routes. See installation for piece-by-piece publishing.
Quickstart
1. Create a flag — from the admin UI at /admin/feature-flags, the CLI, or code:
php artisan flag:create new-dashboard --value=1
2. Check it anywhere:
use Chemaclass\FeatureFlags\Facades\FeatureFlag;
if (FeatureFlag::isEnabled('new-dashboard')) {
// gated code
}
3. In Blade:
@feature('new-dashboard')
<x-new-dashboard />
@else
<x-legacy-dashboard />
@endfeature
4. Guard a route:
Route::get('/dashboard', DashboardController::class)
->middleware('feature.enabled:new-dashboard');
That's the whole loop. Everything below is optional power you can adopt when you need it.
Cookbook
Each recipe is copy-paste; follow the link for the full reference.
use Chemaclass\FeatureFlags\Contracts\FeatureKey;
enum AppFeature: string implements FeatureKey
{
case NewDashboard = 'new-dashboard';
public function key(): string { return $this->value; }
}
FeatureFlag::isEnabled(AppFeature::NewDashboard);
Generate this enum from your existing flags: php artisan flag:generate. → usage
// $scopeId is any string your app decides on. A scoped row beats the global one.
FeatureFlag::isEnabled('new-dashboard', $team->id);
A FeatureScopeResolver can resolve the scope automatically for the middleware and Blade.
→ scopes
FeatureFlag::updateOrCreate(['key' => 'new-billing', 'scope_id' => null], [
'value' => false,
'rules' => [
['when' => [['attr' => 'plan', 'op' => 'eq', 'value' => 'pro']], 'then' => true],
],
]);
FeatureFlag::isEnabled('new-billing', $userId, ['plan' => 'pro']); // true
Operators: eq, neq, in, not_in, gt, gte, lt, lte, contains, starts_with, ends_with.
→ usage
FeatureFlag::updateOrCreate(['key' => 'new-checkout', 'scope_id' => null],
['value' => true, 'rollout_percentage' => 30]);
// enabled for a stable ~30% of scopes
→ usage
$v = FeatureFlag::variant('homepage', $userId);
$v?->name; // 'control' | 'blue'
$v?->payload; // per-variant blob
→ usage
FeatureFlag::allEnabled(['new-dashboard', 'beta-search'], $userId);
// ['new-dashboard' => true, 'beta-search' => false]
- Environments — same key, different value per env. → usage
- Prerequisites — a flag requires others to be on. → usage
- Kill-switch — force keys off via
FEATURE_FLAGS_KILL_SWITCH. → usage - Audit log — persist every toggle + admin history. → extending
- Caching — request memoization always on; set a cache store for cross-request + real-time invalidation. → configuration
Enable the API (feature-flags.api.enabled), then evaluate flags over HTTP — targeting rules,
rollout and variants all apply:
import { createFeatureFlags } from './vendor/feature-flags.js'; // php artisan vendor:publish --tag=feature-flags-js
const ff = createFeatureFlags({ endpoint: '/feature-flags/api/evaluate', scope: user.id, context: { plan: user.plan } });
await ff.load();
ff.isEnabled('new-dashboard'); // bool
ff.variant('homepage'); // { name, payload } | null
→ api
php artisan flag:list --scope=team-1
php artisan flag:create beta-search --value=0 --hint="WIP"
php artisan flag:toggle new-dashboard
php artisan flag:stale --days=30 # flags safe to delete
php artisan flag:generate # typed enum from your keys
php artisan flag:sync --prune # reconcile from a versioned file (config-as-code)
php artisan flag:stats # exposure counts per flag/variant (analytics)
→ usage
Admin UI
Visit /admin/feature-flags (configurable, gated by web+auth by default): flags grouped
by key, sliding toggles, inline editing, scope overrides, dark mode. → admin-ui
What you get
Everything runs on your existing database — no service to run, no vendor lock-in. In one package:
- Evaluation — boolean, per-scope overrides, attribute targeting rules, percentage rollout, time windows, prerequisites, and a global kill-switch.
- Experiments & config — weighted multivariate variants with per-variant payloads.
- Environments — the same key resolving differently per environment.
- Delivery — facade,
@featureBlade directives, route middleware, Artisan commands, and a JSON HTTP API with a zero-dependency JS client for frontends and other services. - Operations — request + cross-request caching with real-time invalidation, lifecycle events, an opt-in audit log with admin history, exposure analytics, stale-flag detection, typed enum codegen, and config-as-code sync.
- Admin UI — a published Blade page, no build step.
It's a self-hosted toolkit for a Laravel app — not a polyglot SaaS with an experimentation stats engine and multi-language SDKs. If you need that, use a hosted platform. Evaluating alternatives (Pennant, Unleash, Flagsmith, LaunchDarkly)? Check their current docs — capabilities and pricing tiers change.
Already on Pennant? The optional bridge lets Feature::active() resolve
through this package, so you can adopt it without rewriting call sites.
Documentation
| Topic | Link |
|---|---|
| Installation & setup | installation.md |
| Configuration reference | configuration.md |
| Defining, checking, targeting & variants | usage.md |
| Scope resolvers | scopes.md |
| Middleware guard | middleware.md |
| HTTP API & JS client | api.md |
| Exposure analytics | analytics.md |
| Admin UI | admin-ui.md |
| Custom repository, audit log, storage | extending.md |
| Laravel Pennant bridge | pennant.md |
| Testing | testing.md |
| Recipes & patterns | recipes.md |
| Architecture overview | architecture.md |
Stability
1.0 and stable — semver from here on, 100% test coverage enforced in CI. Pin with
composer require chemaclass/laravel-feature-flags:^1.0.
Requirements
- PHP
^8.3 - Laravel
^11.0 || ^12.0
This package pulls in illuminate/* and nothing more. Any advisories from composer audit
come transitively from the Laravel framework your app already ships — keep Laravel on its
latest patch release to pick up fixes.
Contributing
See CONTRIBUTING.md for local dev setup, the Docker demo app, the test
suites, formatting, and the release.sh flow.
License
MIT.