Settings
The settings module centralizes operational configuration inside the admin panel.
Sections
general— app name, site display-timezone fallback, active interface languages, logo upload/remove, dashboard welcome message (optional WYSIWYG)auth— registration, email verification, password reset, two-factor availability, login throttle, and password policy (min length, expiry, complexity rules)mail— mailer, SMTP host/port, credentials, from address/namestorage— media disk selection and S3-compatible / AWS credentialsfile_manager— upload size, accepted MIME list, audio/video togglesturnstile— feature toggle, site key, and secret keyapi_integrations— Postman and Apidog sync credentials; two configuration cards in a single tabapi_clients— Passport OAuth2 client listing, creation, update, and deleteapi_tokens— Personal Access Token listing, revocation, and one-time token mintingsystem_health—sk:doctorresults inside Settings for system admins
Storage Model
Settings are stored in the database and resolved through the setting service layer.
Sensitive keys can be encrypted through config/settings.php.
Current examples:
mail.passwordstorage.spaces_secretstorage.aws_secretturnstile.secret_keypostman.api_keyapidog.access_token
Secret values are not exposed back to the frontend. The settings payload uses *_is_set booleans so the UI can show whether a value already exists without returning the raw secret string.
Route Surface
The admin module exposes routes such as:
settings.indexsettings.update.generalsettings.update.authsettings.update.mailsettings.update.storagesettings.update.fileManagersettings.update.turnstilesettings.update.postman—PUT /settings/postmansettings.update.apidog—PUT /settings/apidogsettings.testMailsettings.upload.logo—POST settings/logosettings.delete.logo—DELETE settings/logosystem-health.run—POST /system-health/runfrom the Settings → System Health tab
Runtime Notes
SettingsController@indexdelivers the grouped settings payload plus timezone options and the configured language list- the General timezone is the site fallback for users whose
users.timezoneisnull; a user who explicitly selects a timezone overrides it. See Timezones for the full resolution chain - write operations stay thin: FormRequest -> DTO -> Action
- secret fields in
mail,storage, andturnstilereturnnullplus*_is_setflags instead of the stored values - submitting an empty secret field keeps the current stored or config-backed value
- logo upload/remove is intentionally handled as a small JSON side flow outside the main
SkForm - logo upload/remove now use the standard
ApiResponseenvelope - disabling two-factor in the Auth tab shows a confirmation before the admin submits the change because it affects user security posture
- the Security tab is divided into three sub-tabs: Authentication (registration, email verification, password reset, two-factor, login throttle), Password Policy (minimum length, expiry days, complexity toggles), and Cloudflare Turnstile
auth.login_throttle = '0'does not disable the Fortify login rate limiter — no admin setting is allowed to leave web login unthrottled. It swaps the strictloginlimiter for the more generouslogin-relaxedfloor (defined instubs/app/Providers/FortifyServiceProvider.php); default is'1'(strict limiter). This key only governs the web (Fortify) login — the API auth routes carry a hardcodedthrottle:5,1middleware that this setting never touches.- password policy settings (
password_min_length,password_require_mixed_case,password_require_numbers,password_require_symbols) are applied to every new password viaPasswordValidationRules; existing passwords are never invalidated auth.password_expiry_days > 0enables theEnsurePasswordNotExpiredmiddleware; users whosepassword_changed_atis older than the configured number of days are redirected to a dedicated, guest-style password-expired screen (routepassword.expired) until they update their password; setting0disables expiry- password expiry exempt routes: the password-expired page (
password.expired), logout, two-factor challenge, Fortify password endpoints — redirect loop is not possible users.password_changed_atis stamped on every password write (registration, reset, profile update, admin user create/update); existing users received anow()back-fill at migration time- Turnstile settings drive the auth-form challenge behavior used by login, register, and forgot-password
- API integration settings store Postman and Apidog sync credentials; secret fields are encrypted and use
*_is_setflags like other secrets - API client and token management are separate Settings tabs backed by Passport admin routes; newly created secrets/tokens are displayed once and then cannot be recovered
- System Health is rendered as a Settings tab and receives its report from
sk:doctor --json - test-mail failures are logged server-side and return a generic flash error instead of exposing raw SMTP exception details
- the General tab's
welcome_messagefield is authored throughFB.editor(); content is sanitised throughApp\Support\HtmlSanitizeron write (FormRequestprepareForValidationhook) and again on read (DashboardController defense-in-depth pass) before it renders on the admin dashboard SettingService::setValue()/setGroup()run keys listed in theHTML_SAFE_KEYSwhitelist throughHtmlSanitizer::clean()— FormRequest, tinker, scheduled commands and queued jobs all go through the same sanitizer, so non-sanitised HTML cannot be persisted via the normal setting API
Auth setting keys
The auth group exposes the following keys. Two default values are relevant for every installation:
- Seeder (fresh install) — the value written by
_03_SettingSeederduringsk:install. Only applies to new installations; the seeder never overwrites an existing row. - Runtime fallback (key absent from DB) — the value used by
SettingsDefaultsQuery::auth()when the key does not exist in the database. This is what upgrading installations get before re-seeding. Fallbacks reflect the hardened baseline introduced in v13.6.0:email_verificationandtwo_factordefault to enabled ('1') on the read path for installations that have never seeded those keys.
| Key | Type | Seeder (fresh install) | Runtime fallback (key absent) | Description |
|---|---|---|---|---|
registration |
boolean | '1' |
'1' |
Allow new users to self-register |
password_reset |
boolean | '1' |
'1' |
Allow email-based password reset |
email_verification |
boolean | '0' |
'1' |
Require email verification before login |
two_factor |
boolean | '0' |
'1' |
Enable two-factor authentication |
login_throttle |
boolean | '1' |
'1' |
'1' = strict Fortify login limiter; '0' = relaxed login-relaxed floor (never fully disabled) |
password_min_length |
integer (string) | '10' |
10 |
Minimum password length |
password_expiry_days |
integer (string) | '0' |
0 |
Days before a password expires; 0 = no expiry |
password_require_mixed_case |
boolean | '1' |
'1' |
Require upper and lower case in passwords |
password_require_numbers |
boolean | '1' |
'1' |
Require at least one digit in passwords |
password_require_symbols |
boolean | '1' |
'1' |
Require at least one symbol in passwords |
All values are stored as strings in the database. The SettingsDefaultsQuery::auth() method casts them to the correct PHP types before they reach the frontend or enforcement layer.
HTML-safe keys
Setting keys that hold rich-text content are listed in SettingService::HTML_SAFE_KEYS. Values written to these keys — through any path — are sanitised before hitting the database. Currently tracked:
general.welcome_message
Add new entries when a future setting holds editor-authored HTML. See the FormBuilder Editor field API for the frontend side.
Best Practice
Keep editable operations in dedicated Actions and use request classes for validation. The settings UI should stay thin and reflect the service layer, not replace it.