Going Further

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.

Project Flavor is a deployment-identity switch — "which build of the product is this?". The current flavor is fixed for the lifetime of a deploy: it's read from an env var at boot, not flipped at runtime.

Difference between Feature Flags and Project Flavor
Feature Flags (Pennant)Project Flavor
What it isA runtime capability switchA deployment-identity constant
Use whenA whole capability, domain, or surface area is ON or OFFA sub-feature, field, column, validation rule, or sidebar item differs between builds that otherwise share the capability
Flip at runtime?Yes (per env / tenant / user, eventually)No — fixed per deploy via APP_FLAVOR
ExamplesThe winch_branches domain, internal-invoices groupOne field hidden on order create, an extra column in a table, a different validation rule, a route that doesn't exist in saas
Question to ask"Would the user see a capability appear or disappear?""Would the user see a cosmetic difference inside a capability that still exists?"
Capability → Feature Flag. Cosmetic / configuration → Project Flavor.

Allowed values: winch (default, full product) and saas (white-label fork). Unknown values fall back to winch (fail-open). Single source of truth: APP_FLAVOR env var → config('app.flavor')ProjectFlavor enum → isWinch() / isSaas() helpers.

The shape

use Domain\Core\Enums\ProjectFlavor;

ProjectFlavor::current();          // ProjectFlavor::WINCH | ProjectFlavor::SAAS
ProjectFlavor::current()->isSaas();

isWinch();                          // global helper, equivalent to the above
isSaas();
projectFlavor();                    // returns the enum

Prefer the helpers in route files and Blade. Prefer the enum (ProjectFlavor::current()) inside domain code where you'd import the class anyway.

Hiding a single field on a form (Blade)

Two equivalent options — pick whichever reads better in context:

{{-- helper-based --}}
@if (isWinch())
    <div class="form-group">
        <label>{{ __('inputs.winch_internal_code') }}</label>
        <input name="winch_internal_code" />
    </div>
@endif

{{-- directive-based --}}
@flavor('winch')
    <div class="form-group">
        <label>{{ __('inputs.winch_internal_code') }}</label>
        <input name="winch_internal_code" />
    </div>
@endflavor

@flavor also supports @else:

@flavor('winch')
    <span>Full Winch experience</span>
@else
    <span>SaaS edition</span>
@endflavor

Hiding one column in a table

<thead>
    <tr>
        <th>{{ __('inputs.name') }}</th>
        @if (isWinch())
            <th>{{ __('inputs.winch_branch') }}</th>
        @endif
    </tr>
</thead>

Different validation rule per flavor

public function rules(): array
{
    $rules = [
        'name' => 'required|string',
    ];

    if (isWinch()) {
        $rules['winch_internal_code'] = 'required|string|max:50';
    }

    return $rules;
}

Hiding one sub-menu item (without removing the parent group)

Add the route name to route_flavors in config/cp-menu.php:

'route_flavors' => [
    'cp.some_resource.index' => ['saas'],
],

If every sub-item ends up hidden, CpMenu auto-prunes the parent group.

Hiding a whole sidebar group (when the underlying domain still exists)

'group_flavors' => [
    SomeGroup::class => ['saas'],
],

If the whole domain is off (no routes, no controllers needed), use a Feature Flag instead.

Gating routes by flavor

Use the flavor route middleware (registered as the EnsureFlavor alias in bootstrap/app.php). It mirrors the feature: middleware used by Pennant and returns 404 when the current flavor is not in the allow-list:

Route::middleware('flavor:winch')->group(function () {
    Route::resource('thing', ThingController::class);
});

// Multiple flavors:
Route::middleware('flavor:winch,saas')->group(fn () => ...);

The route still appears in route:list (so route('cp.thing.index') works in Blade and menu helpers and won't blow up at compile time), but any request under the wrong flavor 404s before the controller runs. The sidebar is pruned separately via route_flavors / group_flavors in config/cp-menu.php so hidden routes never appear in the menu.

When the route should literally not exist in a build (e.g., for route:list cleanliness on a saas-only deploy), wrap the registration as well:

if (isWinch()) {
    Route::resource('winch_only_thing', ThingController::class);
}

In most cases the middleware is enough — reach for the registration-time wrapper only when you have a concrete reason to keep the route out of the route table entirely.

Quick lookup

ScenarioUse
One field on a formProjectFlavor (isWinch())
One column in a tableProjectFlavor
One sub-menu itemProjectFlavor (route_flavors)
Whole sidebar group, but the underlying domain still existsProjectFlavor (group_flavors)
Different validation ruleProjectFlavor
Routes reachable only in one flavor (404 otherwise)Route::middleware('flavor:winch')->group(...)
Routes that should literally not exist in a buildif (isWinch()) { Route::... } (registration-time skip)
Whole domain on/offFeature Flag
Per-tenant or per-user toggleFeature Flag

Anti-patterns

  • Branching on ProjectFlavor to gate a whole domain. Use a Feature Flag — it has middleware support, @feature, menu pruning via group_features / route_features, and may be made per-tenant later.
  • Relying on the flavor middleware to keep menu links from rendering. The middleware only 404s the request — it does not prune the sidebar. Always add a matching entry to route_flavors / group_flavors so the menu builder doesn't generate the link in the first place.
  • Hardcoded if (config('app.flavor') === 'winch'). Use isWinch() / ProjectFlavor::current() for type safety and a single source of truth.
  • Using ProjectFlavor for per-user toggles. Flavor is deployment-wide. If different users in the same deploy need different behaviour, that's a Feature Flag (or a real authorization check).

Reference files

  • src/Domain/Core/Enums/ProjectFlavor.php — the enum.
  • config/app.php'flavor' key reading from APP_FLAVOR.
  • config/cp-menu.phpgroup_flavors / route_flavors maps.
  • app/Packages/Menus/CpMenu.php — menu builder that consumes the maps.
  • app/helpers.phpprojectFlavor(), isWinch(), isSaas().
  • bootstrap/app.phpEnsureFlavor middleware alias as flavor:.