← Back to blog

The Laravel Feature Flags Setup I Wish I'd Used From Day One

26 Feb 2026 · 5 min read

The Laravel Feature Flags Setup I Wish I'd Used From Day One

There's a particular kind of pain that only SaaS developers know. You've pushed a new feature to production, a paying customer has found it, and it's broken in a way you absolutely did not anticipate. Your options are either a rushed hotfix or a full rollback — neither of which feels good at half past ten on a Tuesday night.

I've been there more times than I care to admit. And for most of those incidents, a proper feature flag system would have meant the difference between a five-minute toggle and a forty-minute fire drill.

So let me share the setup I've landed on — one that's lightweight, practical, and slots naturally into the Laravel way of doing things.

Why Bother With Feature Flags?

If you're building a SaaS product, feature flags give you a few genuinely valuable things:

  • Safe deployments — ship code to production before it's "live"
  • Beta access — let specific users or accounts try something new
  • Kill switches — turn off a broken feature instantly without a redeploy
  • A/B testing — run experiments without branching your codebase to pieces

The key insight I've come to is that feature flags aren't just a DevOps concern. They're a product development tool. And in a small SaaS team — or when you're a solo developer like I often am — that distinction matters a lot.

Keeping It in Laravel (No External Service Required)

There are great paid services for feature flags. LaunchDarkly is excellent. So is Flagsmith. But when you're in the early stages of a SaaS, you often don't want another monthly subscription or another third-party dependency.

The good news is that Laravel gives you everything you need to build a solid feature flag system yourself, and it honestly doesn't take long.

Here's the approach I use.

Step 1: The Database Table

I start with a simple features table:

Schema::create('features', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->boolean('is_active')->default(false);
    $table->json('metadata')->nullable(); // for per-plan or per-user targeting
    $table->timestamps();
});

The metadata column does a lot of heavy lifting. It lets me store things like which subscription plans have access, or which specific user IDs are in a beta group.

Step 2: A Clean Service Class

I wrap the logic in a FeatureService that gets bound to the container as a singleton:

class FeatureService
{
    protected Collection $features;

    public function __construct()
    {
        $this->features = Cache::remember('features', now()->addMinutes(10), fn () =>
            Feature::all()->keyBy('name')
        );
    }

    public function isActive(string $name, ?User $user = null): bool
    {
        $feature = $this->features->get($name);

        if (! $feature || ! $feature->is_active) {
            return false;
        }

        if ($user && $metadata = $feature->metadata) {
            if (isset($metadata['user_ids'])) {
                return in_array($user->id, $metadata['user_ids']);
            }

            if (isset($metadata['plans'])) {
                return in_array($user->subscription_plan, $metadata['plans']);
            }
        }

        return true;
    }
}

I cache the features for ten minutes to avoid hammering the database on every request, and clearing the cache whenever a feature is updated keeps things fresh:

static::saved(fn () => Cache::forget('features'));

Step 3: A Blade Directive for Clean Templates

This is a small touch that makes a big difference when you're working with Livewire components and Blade views:

Blade::if('feature', function (string $name) {
    return app(FeatureService::class)->isActive($name, auth()->user());
});

Now in any Blade or Livewire template I can write:

@feature('new-dashboard')
    <x-new-dashboard />
@else
    <x-legacy-dashboard />
@endfeature

That reads cleanly, and it means my Livewire components don't need to know anything about the flag system directly.

Managing Flags in the Admin Panel

I wire this up to a simple Livewire component in the admin area. A table of feature flags, each with a toggle. Flip the switch, cache clears, feature is live (or killed) within seconds.

When I'm using Flux UI for the admin, it takes maybe twenty minutes to build a usable flags management screen. Clean toggles, a metadata JSON editor for targeting, timestamps. That's it.

The Workflow Change This Creates

Once you have flags in place, your deployment workflow changes in a really positive way. You stop thinking in terms of "is this feature ready to ship?" and start thinking "is this feature ready to deploy?" Those are different questions.

I can merge code into main, deploy it, and the flag stays off. QA on production. Stakeholder preview. Gradual rollout to a percentage of users. All without separate environments or branch gymnastics.

For a recent client project — a B2B SaaS built on Laravel 12 and Livewire v3 — we used this pattern to roll out a completely redesigned billing flow. We enabled it for the client's own account first, then five beta users, then everyone on the Pro plan, then the whole base. Each step took about thirty seconds. No redeploys. No stress.

What I'd Do Differently If Starting Again

Honestly? I'd add this on day one of every project, not week eight when the first scary deployment comes along. The table, the service, the Blade directive — it's maybe two hours of setup. And the peace of mind is worth far more than that.

If you're building a SaaS on Laravel and you haven't got feature flags in place yet, this weekend is a great time to change that.

As always, if you've got a different approach or you're using one of the managed services and love it, I'd genuinely like to hear about it. Drop me a message or find me on Twitter/X — always happy to talk through this stuff.