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

Laravel Feature Flags Laravel Package

juststeveking/laravel-feature-flags

A lightweight, database-backed feature flag package for Laravel. Define flags, enable or disable them from code or Artisan commands, and gate routes, Blade views, and application logic through a clean facade. Simple to install and easy to drop into existing Laravel projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes):

composer require juststeveking/laravel-feature-flags
php artisan vendor:publish --provider="JustSteveKing\FeatureFlags\FeatureFlagsServiceProvider"
php artisan migrate

Publish both the config file and the migration — if you only publish config you'll get a "base table or view not found" error the first time you evaluate a flag. Some providers expose separate publish tags for config vs. migrations; run php artisan vendor:publish interactively once and read the prompt rather than guessing the tag.

Where to look first, in this order:

  1. config/feature-flags.php — this is where the bindable Feature model and the cache settings live. Two keys will drive most of your day-to-day decisions.
  2. src/Models/Feature.php — the schema in miniature. Read the $fillable/$casts before you write a seeder or a query.
  3. src/Facades/Feature.php and any helper file — confirm the actual method name (isActive(), active(), enabled() — verify against the README; the facade is a thin wrapper, so injecting the model directly is always an option if you dislike the naming).
  4. database/migrations/* — check the generated timestamp against your own migrations for collisions.

Your first use case — ship something dark behind a flag. Create the flag, confirm it's off, then gate a route:

php artisan feature:create new-checkout --description="Owner: @you, expires: 2025-06-01"
php artisan feature:enable new-checkout   # (or leave disabled to ship dark)
use JustSteveKing\FeatureFlags\Facades\Feature;

abort_unless(Feature::isActive('new-checkout'), 404);

Sanity check before you write any code: php artisan tinker → Feature::isActive('new-checkout') and verify it flips when you run feature:enable / feature:disable. If it doesn't flip, you've met the cache. Go to Gotchas.

Implementation Patterns

Never use raw flag strings at call sites. Define constants once and grep for them when you retire the flag:

final class Features
{
    public const NEW_CHECKOUT = 'billing.new-checkout';
    public const BETA_DASHBOARD = 'dashboard.beta';
}

This is the single highest-leverage convention with this package, because an unknown flag name returns inactive rather than throwing. A typo in a controller is indistinguishable from "the feature is off," and you will lose an afternoon to it.

Gate at the entry point, not in the domain. Resolve the flag once — in the controller, the form request, or the middleware — and pass a boolean down:

public function store(StoreOrderRequest $request, OrderService $orders)
{
    return $orders->handle(
        $request->validated(),
        useNewCheckout: Feature::isActive(Features::NEW_CHECKOUT),
    );
}

Sprinkling facade calls through services and models makes the domain layer untestable without the package booted, and hides the branching from anyone reading the class. Two or three evaluation points per feature is the target.

Blade for presentation, never for authorization.

@feature(Features::BETA_DASHBOARD)
    <a href="{{ route('dashboard.beta') }}">Try the new dashboard</a>
@endfeature

Hiding a link is not access control. A flag is globally flippable and has no relationship to who the user is — pair any flag-gated route with your normal can:/policy checks. Use 404 rather than 403 for the "flag off" case so an unreleased feature is indistinguishable from a nonexistent one.

Middleware for whole route groups. The package registers a feature alias; use it for beta sections and internal tooling:

Route::middleware('feature:'.Features::BETA_DASHBOARD)->group(function () {
    // ...
});

This is the cheapest way to gate a chunk of routes, and it fails closed if the flag row is missing.

Queues: decide deliberately where you evaluate. Evaluating at dispatch time and passing the boolean into the payload freezes the flag state at the moment the user acted — the right choice for "this order was placed with new pricing." Evaluating inside the worker lets a flip affect in-flight jobs — the right choice for kill switches. Pick one on purpose; the default (evaluate somewhere deep inside the job) is a source of confusing repros.

Seeders are not optional. Flags are database rows, so a fresh CI environment or a new staging box starts with everything off. Add an idempotent seeder to the deploy pipeline, after migrate:

Feature::firstOrCreate(['name' => Features::NEW_CHECKOUT], [
    'enabled' => false,
    'description' => 'Owner: @you, expires: 2025-06-01',
]);

Without this, staging silently behaves as if the feature doesn't exist, and the "it works locally" / "it's missing in staging" bug reports start.

Tests. Refresh the database in setUp, create the flag in both states, and assert both paths for anything touching checkout, pricing, or auth:

Feature::create(['name' => Features::NEW_CHECKOUT, 'enabled' => true]);

The rollout ritual. Ship dark (flag off, merged to main) → enable in staging → smoke test → enable in prod → monitor → schedule removal. For destructive features the order is fixed: migrate schema → flip flag on → delete the old code path and the flag row together. Flipping before the migration ships is a guaranteed 500.

Admin surface: don't build one yet. php artisan feature:enable|disable on the box (or as a deploy/ops job) is sufficient for a modest flag inventory. If a UI becomes necessary it's a handful of lines on top of the model — but that's your build, and it comes with the authorization problem you now own.

Gotchas and Tips

  • Silent-false is the design. Unknown flag → inactive. This is the safe direction for production and the hostile direction for development. Mitigate with constants, a seeder, and a five-second tinker check whenever a feature "isn't there."
  • State is cached. A toggle may not take effect immediately, and the interaction with config:cache, Octane, and long-running queue workers needs to be verified against your setup, not assumed. If you're on Octane or a worker fleet, confirm where invalidation happens on enable/disable before you promise anyone an instant kill switch — and clear the cache manually the first time you're surprised.
  • Config cache ≠ flag state. Flag values live in the database and cache; only package settings (model binding, cache store/TTL) live in config/feature-flags.php. When you change the bindable model, you need config:clear / a redeploy — a stale cached config pointing at the wrong model produces very confusing results.
  • Migration timestamp collisions. Check the published migration's filename against your own before running. If you already have a features table, resolve it before migrate rather than after.
  • Flags are not permissions. Never gate a security boundary (refund, impersonate, export-pii) on a flag. Flipping prod is effectively a deploy and should be permissioned like one — most teams get this wrong exactly once.
  • Global, not per-user. Every read is "on for everyone, right now." When a stakeholder asks for a 5% rollout or "just for Acme Corp," the honest answer with this package is a staged manual flip, not a cohort. Say that before it becomes a sales commitment; Laravel Pennant or a custom resolver is the right tool for real segmentation.
  • No audit trail out of the box. There's no enabled_by / enabled_at history. The model is bindable in config, so you can subclass it with audit columns and a scope or two — but budget that as a build, not a configuration toggle.
  • Flag debt compounds. Every flag doubles the number of code paths you must reason about and test. Put an owner and an expiry date in the description at creation time, review the flag list on a schedule, and delete the flag in the same PR that deletes the losing branch. Treat "flag exists after 90 days" as a bug.
  • Debugging checklist for "the flag isn't working": (1) row exists in the DB? (2) enabled is true? (3) name string matches exactly — case, dots, dashes? (4) cache cleared? (5) config cached with a stale model binding? (6) are you evaluating in a different process (queue worker, Octane worker) that has its own cache?
  • Extension points worth knowing: the bindable model in config (scopes, casts, audit columns), the cache store setting (point it at Redis in prod if you're on file cache), and the fact that the whole package is small and MIT — reading it end-to-end takes an hour, and forking is a legitimate answer when you outgrow it.
  • Verify the PHP floor. 1.4.0 added Laravel 11 support, which implies PHP 8.2+; confirm the constraint in composer.json against your runtime before you standardize, and confirm the caching behavior above with a branch deploy before rolling it across services.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core