Skip to content

Feature Flags Middleware

Feature flag middleware controls access to features at different organizational levels: advisor, organization, and setting-based disabling. This enables gradual rollouts and per-customer feature customization.

AliasClassScope
featureFeatureAdvisor-level features
organization.featureOrganizationFeatureOrganization-level features
disable_features_by_advisor_settingDisableFeaturesByAdvisorSettingSetting-based feature disabling
payment.checkCheckIfPaymentRequiredLicense/payment validation

File: app/Http/Middleware/Feature.php

Checks if a feature is enabled for the context advisor (not login user).

/**
* Check whether the context (as opposed to login) advisor has certain feature enabled
*
* If the login user is a client, will check their advisor instead
*/
final class Feature
{
public function handle(Request $request, Closure $next, string $feature): Response
{
$locality = LocalityManager::locality();
if ($locality instanceof AdvisorLocality) {
$advisor = $locality->getAdvisorOrFail();
} else { // team and member controller
$advisor = Auth::getUserOrFail()->advisor;
throw_unless($advisor !== null);
}
if (($advisor->features[$feature] ?? false) !== true) {
throw new AccessDeniedHttpException(
'The ' . str_replace('_', ' ', $feature) . ' feature is not enabled for this advisor.'
);
}
return $next($request);
}
}
  • Uses LocalityManager::locality() to get context
  • If AdvisorLocality: gets advisor from locality
  • Otherwise: gets advisor from Auth::getUserOrFail()->advisor (for team/member controllers)
  • Checks $advisor->features[$feature] for true value
  • Underscores in feature name replaced with spaces in error message
Route::group(['middleware' => 'feature:risk'], function () {
Route::get('risk-analysis', [RiskController::class, 'show']);
});
Route::group(['middleware' => 'feature:tax_planning'], function () {
Route::resource('tax-scenarios', TaxScenarioController::class);
});
{
"message": "The tax planning feature is not enabled for this advisor."
}

HTTP Status: 403 Forbidden


File: app/Http/Middleware/OrganizationFeature.php

Checks if a feature is enabled at the organization level.

/**
* Check whether the context (as opposed to login) organization has certain organization feature enabled
*/
final class OrganizationFeature
{
public function handle(Request $request, Closure $next, string $feature): Response
{
$locality = LocalityManager::locality();
if ($locality instanceof OrganizationLocality) {
$organization = $locality->getOrganizationOrFail();
} elseif ($locality instanceof AdvisorLocality) {
$organization = $locality->getAdvisorOrFail()->organization;
if ($organization === null) {
throw new RuntimeException('Login advisor must belong to an organization.');
}
} else {
throw new AccessDeniedHttpException(
'The ' . $locality::class . ' locality is not supported by this route.'
);
}
$organization->loadMissing(Organization::RELATION_BILLING_SUBSCRIPTIONS);
if (($organization->features[$feature] ?? false) !== true) {
throw new AccessDeniedHttpException(
'The ' . str_replace('_', ' ', $feature) . ' feature is not enabled for this organization.'
);
}
return $next($request);
}
}
  • Supports two locality types:
    • OrganizationLocality: gets organization directly
    • AdvisorLocality: gets organization via advisor (throws if advisor has no organization)
  • Other locality types throw AccessDeniedHttpException
  • Eager loads billing subscriptions via loadMissing(Organization::RELATION_BILLING_SUBSCRIPTIONS)
  • Checks $organization->features[$feature] for true value
Route::group([
'middleware' => 'organization.feature:risk_management'
], function () {
Route::get('org/risk-dashboard', [OrgRiskController::class, 'dashboard']);
});
Route::group([
'middleware' => 'organization.feature:sso'
], function () {
Route::get('org/sso/settings', [SsoController::class, 'show']);
});
// Feature not enabled
{
"message": "The risk management feature is not enabled for this organization."
}
// Unsupported locality
{
"message": "The App\\Locality\\HouseholdLocality locality is not supported by this route."
}
// Advisor without organization
RuntimeException: "Login advisor must belong to an organization."

HTTP Status: 403 Forbidden


File: app/Http/Middleware/DisableFeaturesByAdvisorSetting.php

Disables features based on advisor settings. Only supports specific settings via match statement.

final class DisableFeaturesByAdvisorSetting
{
public function handle(Request $request, Closure $next, string $setting): Response
{
$advisor = LocalityManager::advisorLocality()->getAdvisorOrFail();
$error_msg = match ($setting) {
Advisor::SETTINGS_DISABLE_VAULT => 'The vault feature is not enabled for this advisor.',
Advisor::SETTINGS_DISABLE_VAULT_SHARING => 'The vault share feature is not enabled for this advisor.',
default => throw new InvalidArgumentException("Setting [$setting] is not supported."),
};
if (($advisor->settings[$setting] ?? false) === true) {
throw new AccessDeniedHttpException($error_msg);
}
return $next($request);
}
}
  • Uses LocalityManager::advisorLocality()->getAdvisorOrFail() (specific method, not locality())
  • Only supports TWO settings via match statement:
    • Advisor::SETTINGS_DISABLE_VAULT
    • Advisor::SETTINGS_DISABLE_VAULT_SHARING
  • Throws InvalidArgumentException for unsupported settings
  • Checks $advisor->settings[$setting] for true value (disabled)
ConstantValueError Message
Advisor::SETTINGS_DISABLE_VAULTdisable_vault”The vault feature is not enabled for this advisor.”
Advisor::SETTINGS_DISABLE_VAULT_SHARINGdisable_vault_sharing”The vault share feature is not enabled for this advisor.”
// Vault features (can be disabled)
Route::group([
'middleware' => 'disable_features_by_advisor_setting:disable_vault'
], function () {
Route::resource('vault', VaultController::class);
// Nested: vault sharing (can also be disabled separately)
Route::group([
'middleware' => 'disable_features_by_advisor_setting:disable_vault_sharing'
], function () {
Route::post('vault/{document}/share', [VaultShareController::class, 'store']);
});
});
// Vault disabled
{
"message": "The vault feature is not enabled for this advisor."
}
// Vault sharing disabled
{
"message": "The vault share feature is not enabled for this advisor."
}

HTTP Status: 403 Forbidden

For unsupported settings:

InvalidArgumentException: Setting [unknown_setting] is not supported.

File: app/Http/Middleware/CheckIfPaymentRequired.php

Validates that the advisor has a valid license via the model’s checkLicenseStatus() method.

final class CheckIfPaymentRequired
{
public function handle(Request $request, Closure $next): Response
{
$advisor_id = Auth::getUserOrFail()->advisor_user_id;
throw_unless($advisor_id !== null);
Advisor::findOrFail($advisor_id)->checkLicenseStatus();
return $next($request);
}
}
  • Uses Auth::getUserOrFail()->advisor_user_id (NOT LocalityManager)
  • Throws if advisor_user_id is null
  • Delegates to Advisor::checkLicenseStatus() method for actual validation
  • The checkLicenseStatus() method throws appropriate exceptions if license is invalid
Route::group(['middleware' => 'payment.check'], function () {
Route::resource('advanced-reports', AdvancedReportController::class);
Route::post('export/pdf', [ExportController::class, 'pdf']);
});

Depends on Advisor::checkLicenseStatus() implementation, typically:

HTTP Status: 402 Payment Required or 403 Forbidden


Organization Feature (org-level)
Advisor Feature (advisor-level)
Advisor Setting (disable override)
Payment Check (license status)
Route::group([
'middleware' => [
'organization.feature:risk_management',
'feature:risk',
'payment.check',
]
], function () {
// Requires:
// 1. Organization has risk_management feature
// 2. Advisor has risk feature enabled
// 3. Advisor has valid license
Route::resource('risk-assessments', RiskAssessmentController::class);
});

// Database: advisors.features (JSON)
{
"risk": true,
"tax_planning": true,
"vault": true
}
// Access in middleware
if (($advisor->features[$feature] ?? false) !== true) { ... }
// Database: organizations.features (JSON)
{
"risk_management": true,
"compliance": true,
"sso": true
}
// Access in middleware
if (($organization->features[$feature] ?? false) !== true) { ... }
// Database: advisors.settings (JSON)
{
"disable_vault": false,
"disable_vault_sharing": true
}
// Access in middleware
if (($advisor->settings[$setting] ?? false) === true) { ... }

Request
┌────────────────────────┐
│ Organization Feature? │────No────▶ 403
│ (if middleware applied)│
└────────────────────────┘
│ Yes
┌────────────────────────┐
│ Advisor Feature? │────No────▶ 403
│ (if middleware applied)│
└────────────────────────┘
│ Yes
┌────────────────────────┐
│ Disabled by Setting? │────Yes───▶ 403
│ (vault/vault_sharing) │
└────────────────────────┘
│ No
┌────────────────────────┐
│ License Valid? │────No────▶ 402/403
│ (checkLicenseStatus) │
└────────────────────────┘
│ Yes
Continue

MiddlewareLevelLogicSource
feature:XAdvisorCheck $advisor->features[X] === trueLocalityManager or Auth
organization.feature:XOrganizationCheck $organization->features[X] === trueLocalityManager (OrganizationLocality or AdvisorLocality)
disable_features_by_advisor_setting:XAdvisorBlock if $advisor->settings[X] === trueLocalityManager::advisorLocality()
payment.checkAdvisorDelegate to checkLicenseStatus()Auth::getUserOrFail()->advisor_user_id