---
url: /internals/settings-overlay.md
---
# Settings overlay

The settings store (2.12) keeps public business settings in the database and lays them over the configuration at boot, so every consumer of `config('services.google.gtm-id')` sees the stored value without knowing the store exists. The user side is on [Settings store](/panel/settings-store); this page is how it works.

## Classes

`core/src/Settings/`:

| Class | Role |
|---|---|
| `EvolveSettings` | base class on `spatie/laravel-settings`; each subclass declares `group()` and a `bridge()` map from property to `[config key, env variable]`. Static helpers `sources()` (where each property resolves from) and `awaitsImport()` (nothing stored, but the config holds a non-default value) |
| `TrackingSettings`, `SeoSettings`, `SiteSettings`, `BrandingSettings`, `CompanySettings`, `MailSettings` (2.18) | the six groups |
| `SettingsOverlay` | reads every stored property and writes the non-null ones into the config |
| `PackageDefaults` | the values the shipped config resolves with no variable set, used by the import to skip defaults |

Storage is the `evolve_settings` table through the spatie repository named `evolve`.

## When the overlay runs

`EvolveServiceProvider::registerSettingsStore()` registers the overlay in an `app->booting` callback: after every provider merged its config, before any provider boots and reads it. It runs on every boot, web and console, on top of a cached config when there is one. It is skipped while `config:cache` or `optimize` build the cache, so stored values are never baked into the cache file: a change in the store takes effect on the next request without a rebuild. It is also skipped during `package:discover`, which runs on build machines without a database.

A `null` property leaves the config alone. The property values are read in one query and cached under `evolve.settings.v1`; warm boots run no query. Without the table (before `migrate`) nothing is applied and a `missing` marker is cached, which the next migration clears (`MigrationsEnded` listener calls `forget()`). `evolve:db:copy` and the Envoy database tasks forget the cache on the side that received the database (2.12.1).

## Resolution and sources

`SettingsOverlay::source($configKey)` answers where a resolved value comes from: `settings`, `config`, or `null`. `configValue()` returns what the `.env` and config files give the key underneath the overlay; the panel uses it for the "inherits …" placeholders. The health endpoint's `tracking.sources` and `mail.sources`, the meta of the tracking and mailer checks, and the shadowed-config check use these (a shadowed key that a stored setting resolves is reported under `resolved_by_settings` and does not warn).

## Writing

Since 2.18 the panel writes the store. `Http\Livewire\SettingsManager` lists the groups it edits in `STORED_SETTINGS`; inputs are named `<group>.<property>`. `STORED_PROPERTIES` limits a group to some properties (branding: panel colour, login image, logo), `STORED_RULES` adds validation beyond the types, and casts follow the property types by reflection (int for media IDs, bool for switches). `storedChanges()` validates and saves before the `.env` part runs, `consentCanBeGranted()` resolves the posted store values (posted, inherited for "inherit", current when not posted) and refuses opt-in without banner or CMP. Stored keys are removed from the incoming data, so the bridged env variables are never written; an outdated view copy posting them has them dropped by `reportDroppedKeys()`. Store-only saves skip the config cache rebuild.

Other writes come from `evolve:settings:import` (`core/src/Console/Commands/ImportSettings.php`: reads the config before the overlay, skips package defaults unless `--include-defaults`, casts to the property type, never edits the `.env`, logs one `settings-imported` activity) and from code through the settings classes. A property set back to `null` falls back to the `.env`.

## Adding a setting

Add a nullable property to the group class and a line to its `bridge()` with the config key it overlays and the env variable the import reads. Register the property's row with a spatie settings migration (`$this->migrator->add('tracking.new_property')`), as `core/database/migrations/2026_09_26_000000_create_evolve_settings_table.php` does for the existing ones and `2026_10_13_100000_add_mail_settings` for a whole group (use `insertOrIgnore` semantics, rows may exist). The overlay, the import and the health sources pick the property up from the bridge. To make it editable, add the input `<group>.<property>` to `settings/index.blade.php` (with an entry in `ViewOverrides::CHANGES`), a rule in `STORED_RULES` when the type is not enough, and for a new group the class in `STORED_SETTINGS`.
