Skip to content
SaaS4Builders
Settings

Settings Overview

Hierarchical settings system with three-level cascade (Application, Tenant, User), centralized key registry, scope-aware resolution, and full-stack API support.

SaaS4Builders ships a hierarchical settings system that manages configuration at three levels: Application (platform-wide defaults), Tenant (organization preferences), and User (individual preferences). The most specific scope always wins — if a user sets their locale to Italian but the tenant default is French, the user sees Italian.

All valid setting keys are defined in a single PHP enum (SettingKey), which serves as the central registry for key names, allowed scopes, types, defaults, and validation rules. There is no external configuration file — the code is the source of truth.


The Three-Level Cascade

Settings are resolved through a three-level hierarchy where more specific scopes override less specific ones:

  • Application — Platform-wide defaults set by the platform administrator. These apply to every tenant and user unless overridden. Examples: app name, company information, default locale.
  • Tenant — Organization-level preferences set by tenant admins. Override application defaults for all members of the tenant. Examples: preferred locale, timezone, currency.
  • User — Individual preferences set by each user. Override both tenant and application values. Examples: personal locale, timezone, notification preferences.

If no value is found at any level, the system falls back to the code-level default defined in the setting's definition.

Not every setting supports all three scopes. For example, company billing information (billing.company.*) is only available at the application scope, while notification preferences (notifications.*) are only available at the user scope. The SettingKey enum defines which scopes each setting supports.

Setting Keys

All valid setting keys are defined in the SettingKey enum. Each case maps to a dot-notation string identifier:

backend/app/Enums/SettingKey.php
enum SettingKey: string
{
    // Application settings
    case APP_NAME = 'app.name';
    case APP_SUPPORT_EMAIL = 'app.support_email';

    // Internationalization
    case I18N_LOCALE = 'i18n.locale';
    case I18N_FALLBACK_LOCALE = 'i18n.fallback_locale';

    // Time
    case TIME_TIMEZONE = 'time.timezone';

    // Money
    case MONEY_CURRENCY = 'money.currency';
    case MONEY_FALLBACK_CURRENCY = 'money.fallback_currency';

    // Company Information
    case BILLING_COMPANY_NAME = 'billing.company.name';
    case BILLING_COMPANY_ADDRESS = 'billing.company.address';
    case BILLING_COMPANY_ADDRESS_LINE2 = 'billing.company.address_line2';
    case BILLING_COMPANY_CITY = 'billing.company.city';
    case BILLING_COMPANY_STATE = 'billing.company.state';
    case BILLING_COMPANY_POSTAL_CODE = 'billing.company.postal_code';
    case BILLING_COMPANY_COUNTRY = 'billing.company.country';
    case BILLING_COMPANY_VAT_NUMBER = 'billing.company.vat_number';

    // Branding
    case APP_LOGO = 'app.logo';
    case APP_EMAIL_LOGO = 'app.email_logo';

    // Email
    case MAIL_FROM_ADDRESS = 'mail.from.address';
    case MAIL_FROM_NAME = 'mail.from.name';
    case MAIL_REPLY_TO_ADDRESS = 'mail.reply_to.address';
    case MAIL_REPLY_TO_NAME = 'mail.reply_to.name';
    case APP_SUPPORT_NAME = 'app.support_name';

    // Notifications
    case NOTIFICATIONS_EMAIL_ENABLED = 'notifications.email_enabled';
    case NOTIFICATIONS_DESKTOP_ENABLED = 'notifications.desktop_enabled';
}

The 23 keys are organized into eight groups:

Application Settings

KeyDescriptionDefault
app.namePlatform display nameFrom config('app.name')
app.support_emailSupport contact emailsupport@example.com

Internationalization

KeyDescriptionDefault
i18n.localeActive locale (en, fr, es, it)en
i18n.fallback_localeFallback when translation is missingen

Time

KeyDescriptionDefault
time.timezoneIANA timezone identifierUTC

Money

KeyDescriptionDefault
money.currencyDefault currency (3-letter ISO 4217)EUR
money.fallback_currencyFallback when currency is unavailableEUR

Company Information

KeyDescriptionDefault
billing.company.nameLegal business nameYour Company
billing.company.addressPrimary address lineEmpty
billing.company.address_line2Secondary address linenull
billing.company.cityCityEmpty
billing.company.stateState or provincenull
billing.company.postal_codeZIP / postal codeEmpty
billing.company.countryISO 3166-1 alpha-2 country codeFR
billing.company.vat_numberVAT / tax IDEmpty

Branding

KeyDescriptionDefault
app.logoApplication logo file pathnull
app.email_logoEmail template logo file pathnull

Email

KeyDescriptionDefault
mail.from.addressSender email addressFrom config('mail.from.address')
mail.from.nameSender display nameFrom config('mail.from.name')
mail.reply_to.addressReply-to email addressnull
mail.reply_to.nameReply-to display namenull
app.support_nameSupport contact display namenull

Notifications

KeyDescriptionDefault
notifications.email_enabledEmail notifications enabledtrue
notifications.desktop_enabledDesktop notifications enabledfalse

Scope Matrix

This table defines which scopes each setting supports. A checkmark means the setting can be set at that scope:

Setting KeyApplicationTenantUser
app.name
app.support_email
i18n.locale
i18n.fallback_locale
time.timezone
money.currency
money.fallback_currency
billing.company.name
billing.company.address
billing.company.address_line2
billing.company.city
billing.company.state
billing.company.postal_code
billing.company.country
billing.company.vat_number
app.logo
app.email_logo
mail.from.address
mail.from.name
mail.reply_to.address
mail.reply_to.name
app.support_name
notifications.email_enabled
notifications.desktop_enabled

Key observations:

  • Two settings support all three scopes: i18n.locale, time.timezone
  • One setting supports application + tenant: money.currency
  • Two settings are user-only: notifications.email_enabled, notifications.desktop_enabled
  • All remaining settings are application-only (platform admin managed)

Resolution Algorithm

The SettingsResolver service implements the cascade. When resolving a setting, it checks scopes from most specific to least specific:

backend/app/Services/SettingsResolver.php
public function resolve(SettingKey $key, ?Tenant $tenant = null, ?User $user = null): array
{
    $definition = $key->definition();

    // 1. Try user scope (highest priority)
    if ($user !== null && $key->allowedAtScope(SettingScope::User)) {
        $value = $this->userSettings->get($user->id, $key);
        if ($value !== null) {
            return ['value' => $value, 'source' => 'user'];
        }
    }

    // 2. Try tenant scope
    if ($tenant !== null && $key->allowedAtScope(SettingScope::Tenant)) {
        $value = $this->tenantSettings->get($tenant->id, $key);
        if ($value !== null) {
            return ['value' => $value, 'source' => 'tenant'];
        }
    }

    // 3. Try application scope
    if ($key->allowedAtScope(SettingScope::Application)) {
        $value = $this->appSettings->get($key);
        if ($value !== null) {
            return ['value' => $value, 'source' => 'application'];
        }
    }

    // 4. Fall back to registry default
    return ['value' => $definition->default, 'source' => 'default'];
}

Every resolved setting returns both the value and its source (which scope it came from: user, tenant, application, or default). This lets the frontend show users where a setting originates — for example, displaying "inherited from organization" when a tenant-level setting is in effect.


Setting Definitions

Each setting key carries a SettingDefinition value object that describes its metadata:

backend/app/Settings/SettingDefinition.php
final readonly class SettingDefinition
{
    public function __construct(
        public array $scopes,               // Allowed scopes (e.g., [Application, Tenant, User])
        public string $type,                // Storage type: string, bool, int, enum, email, timezone, currency
        public mixed $default,              // Default value when not set in database
        public ?array $allowedValues = null, // For enum types: list of valid values
        public ?string $validation = null,  // Additional Laravel validation rules
        public bool $sensitive = false,     // If true, excluded from public API responses
    ) {}
}

For example, the locale setting is defined as:

backend/app/Enums/SettingKey.php
self::I18N_LOCALE => new SettingDefinition(
    scopes: [SettingScope::Application, SettingScope::Tenant, SettingScope::User],
    type: 'enum',
    default: config('app.locale', 'en'),
    allowedValues: ['en', 'fr', 'es', 'it'],
),

See Defining Settings for the full guide on adding new settings, supported types, and validation.


Architecture Overview

The settings feature spans both backend and frontend:

backend/app/
├── Enums/
│   ├── SettingKey.php                  # Central registry of all setting keys
│   └── SettingScope.php                # Application, Tenant, User enum
├── Settings/
│   └── SettingDefinition.php           # Value object: scopes, type, default, validation
├── Casts/
│   └── SettingValueCast.php            # JSON {"v": ...} wrapper for type-safe storage
├── Models/
│   ├── ApplicationSetting.php          # Platform-wide settings model
│   ├── TenantSetting.php              # Tenant-scoped settings model
│   └── UserSetting.php                # User-scoped settings model
├── Repositories/
│   ├── ApplicationSettingsRepository.php
│   ├── TenantSettingsRepository.php
│   └── UserSettingsRepository.php
├── Services/
│   ├── SettingsResolver.php            # Cascaded resolution logic
│   └── SettingsCacheManager.php        # Redis caching with scope-specific TTLs
├── Http/
│   ├── Controllers/Api/V1/
│   │   ├── Settings/                   # Effective, User, Tenant, Admin controllers
│   │   ├── Admin/Settings/             # Admin view + logo management
│   │   └── Public/Settings/            # Public settings (no auth)
│   └── Requests/Settings/              # Validation with scope enforcement
└── Application/Admin/Actions/
    ├── UploadSettingsLogo.php
    └── DeleteSettingsLogo.php
frontend/
├── features/foundation/docs/settings/
│   ├── types.ts                        # TypeScript types and payloads
│   ├── schemas.ts                      # Zod validation schemas
│   ├── index.ts                        # Barrel exports
│   ├── stores/
│   │   └── useSettingsStore.ts         # Pinia store (load, update, cache)
│   └── composables/
│       ├── useSettings.ts              # Read facade (get, getWithSource)
│       ├── useUserSettings.ts          # User-level mutations
│       ├── useTenantSettings.ts        # Tenant-level mutations
│       ├── useSettingsLocale.ts        # i18n integration + locale switching
│       ├── useSettingsTimezone.ts      # Timezone + date formatting
│       └── useSettingsCurrency.ts      # Currency + money formatting
├── features/product/platform/
│   ├── composables/
│   │   ├── useAdminSettings.ts         # Platform admin read
│   │   └── useAdminSettingsActions.ts  # Platform admin mutations
│   └── components/
│       ├── CompanySettingsForm.vue
│       ├── EmailSettingsForm.vue
│       ├── RegionalSettingsForm.vue
│       └── BrandingSettingsForm.vue
└── app/plugins/
    ├── 00.settings-public.ts           # SSR: loads app name + logo
    └── settings.client.ts              # Client: loads on auth, syncs i18n
LayerKey FilesPurpose
DomainSettingKey, SettingScopeType-safe enums for keys and scopes
DomainSettingDefinitionMetadata per key: scopes, type, validation, default
DataApplicationSetting, TenantSetting, UserSettingEloquent models, one per scope
DataSettingValueCastJSON wrapper for type-safe storage
Repository*SettingsRepositoryCRUD operations per scope with cache integration
ServiceSettingsResolverCascaded resolution: User > Tenant > Application > default
ServiceSettingsCacheManagerRedis caching with automatic invalidation
HTTPControllers, RequestsAPI endpoints with scope-aware validation
FrontenduseSettingsStorePinia store: load, update, promise deduplication
FrontendComposablesRead/write facades with i18n, timezone, currency integration

API Endpoints

The settings system exposes eight API endpoints across four authorization levels:

MethodPathAuthPurpose
GET/api/v1/docs/settings/publicNonePublic settings (app name, logo, currency)
GET/api/v1/docs/settings/effectiveAuthenticatedAll settings with cascade resolution
PATCH/api/v1/user/docs/settingsAuthenticatedUpdate user preferences
PATCH/api/v1/tenant/docs/settingsTenant + tenant.updateUpdate tenant settings
GET/api/v1/admin/docs/settingsPlatform adminAll application settings with metadata
PATCH/api/v1/admin/docs/settingsPlatform adminUpdate application settings
POST/api/v1/admin/docs/settings/logoPlatform adminUpload app or email logo
DELETE/api/v1/admin/docs/settings/logo/{type}Platform adminDelete a logo

See Reading & Writing Settings for detailed request/response examples for each endpoint.


What's Next