Settings Store 2.12
The settings store keeps a site's public business configuration in the database instead of the .env: tracking IDs and consent, search engine verification codes, the site name, branding, mail addresses and the company identity. Stored values are laid over the Laravel config at boot, so every consumer keeps reading config(...) and does not need to know where a value came from.
Why a store: the .env is a deployment file. The settings panel rewriting it on a running server has lost values across deploys and been shadowed by project config files, and there was no place at all for the company identity. The store gives those values one home that survives deploys and database copies, while secrets stay in the .env.
Since 2.18 the panel writes the store
The Settings screen saves the fields of these groups (all but the email logo) to the store and no longer writes their .env variables. Existing sites take their .env values over once with evolve:settings:import; until then nothing breaks, the .env keeps applying and the panel shows an import hint. In 2.12 to 2.17 the store existed but the panel still wrote the .env.
Settings classes
Six classes under Racerfish\Evolve\Settings, built on spatie/laravel-settings with their own repository and their own table evolve_settings (a project's own settings table is not touched). Every property is nullable. Each property is bridged to one config key and, where one exists, the env variable behind it.
TrackingSettings (group tracking)
| Property | Config key | Env variable |
|---|---|---|
gtm_id | services.google.gtm-id | GTM_ID |
ga4_measurement_id | services.google.analytics-measurement-id | ANALYTICS_MEASUREMENT_ID |
meta_pixel_id | services.facebook.pixel-id | FACEBOOK_PIXEL_ID |
pinterest_tag_id | services.pinterest.tag-id | PINTEREST_TAG_ID |
hotjar_site_id | services.hotjar.site-id | HOTJAR_SITE_ID |
linkedin_partner_id | services.linkedin.partner-id | LINKEDIN_PARTNER_ID |
linkedin_lead_conversion_id | services.linkedin.lead-conversion-id | LINKEDIN_LEAD_CONVERSION_ID |
conversion_data | services.google.conversion-data | ANALYTICS_CONVERSION_DATA |
server_side (bool) | evolve.tracking.server-side | TRACKING_SERVER_SIDE |
consent_policy | evolve.tracking.consent.policy | TRACKING_CONSENT_POLICY |
cookie_banner (bool) | evolve.cookie-consent | APP_COOKIE_CONSENT |
consent_via_cmp (bool, 2.18) | evolve.tracking.consent.via-cmp | TRACKING_CONSENT_VIA_CMP |
consent_cmp (string, 2.19) | evolve.tracking.consent.cmp | TRACKING_CMP |
cookiebot_id (string, 2.19) | evolve.tracking.consent.cookiebot-id | COOKIEBOT_ID |
API secrets (ANALYTICS_API_SECRET, FACEBOOK_CONVERSIONS_API_KEY) and TRACKING_EMIT are not part of the store. Secrets stay in the .env; TRACKING_EMIT is posture, see Environment Variables.
SeoSettings (group seo)
| Property | Config key | Env variable |
|---|---|---|
google_site_verification | services.google.site-verification | GOOGLE_SITE_VERIFICATION |
bing_site_verification | services.bing.site-verification | BING_SITE_VERIFICATION |
pinterest_site_verification | services.pinterest.site-verification | PINTEREST_SITE_VERIFICATION |
SiteSettings (group site)
| Property | Config key | Env variable |
|---|---|---|
name | app.name | APP_NAME |
ga_property_id | analytics.property_id | ANALYTICS_PROPERTY_ID |
name overlays app.name only. The mail sender is the mail group below.
BrandingSettings (group branding)
| Property | Config key | Env variable |
|---|---|---|
panel_color | evolve.theme-color | APP_BACKEND_THEME_COLOR |
login_image_id (int) | evolve.login-image-id | APP_LOGIN_IMAGE_ID |
logo_media_id (int) | evolve.branding.logo-media-id | none |
email_logo_media_id (int) | evolve.branding.email-logo-media-id | none |
The panel edits panel_color and login_image_id on the General tab and logo_media_id on the Company tab. email_logo_media_id has no field.
MailSettings (group mail) 2.18
| Property | Config key | Env variable |
|---|---|---|
from_address | mail.from.address | MAIL_FROM_ADDRESS |
from_name | mail.from.name | MAIL_FROM_NAME |
reply_to_address | evolve.mail.reply-to | EVOLVE_MAIL_REPLY_TO_ADDRESS |
notification_address | evolve.mail.notification-address | EVOLVE_MAIL_NOTIFICATION_ADDRESS |
alert_address | evolve.mail.alert-address | HEALTH_WEBMASTER_ADDRESS |
The notification address receives new leads of forms without their own administrator address. The alert address receives technical alerts such as health check failures. Mailer and SMTP settings stay in the .env. What each address does is on Mail.
CompanySettings (group company)
| Property | Config key |
|---|---|
legal_name | evolve.company.legal-name |
brand_name | evolve.company.brand-name |
street | evolve.company.street |
postal_code | evolve.company.postal-code |
locality | evolve.company.locality |
region | evolve.company.region |
country | evolve.company.country |
phone | evolve.company.phone |
email | evolve.company.email |
same_as (array) | evolve.company.same-as |
founding_date | evolve.company.founding-date |
The evolve.branding.* and evolve.company.* keys have null defaults (same-as defaults to []) and no env variable. Since 2.18 the Company tab of the panel edits them, and the theme builds the site-wide schema.org Organization from them once the legal name is set, see SEO and Head.
Precedence
For every bridged config key, the first of these that is set wins:
- The stored value in
evolve_settings, when it is notnull. - The
.envand the config files, as Laravel resolves them. - The package default from the shipped config file.
null in the store means "not stored, fall back". Setting a property back to null and saving restores the config value on the spot.
Only null falls back
An empty string '' or an empty array [] stored in a property is a value, and it overrides the .env. Store null to clear a setting, never ''.
How the overlay works
SettingsOverlay runs in an app->booting() callback, after every provider has registered its config and before any provider boots and reads it. It runs on web requests and in the console alike.
- Reads are cached. The stored values are read with one query and kept in the application cache under
evolve.settings.v1, without expiry. A warm boot costs no query (with a database cache store, one cache query). - Saving refreshes. Saving any of the settings classes forgets the cache entry and applies the overlay again in the same request.
- Missing table. Before
php artisan migratehas createdevolve_settings, nothing is applied and a "missing" marker is cached. The marker is forgotten when migrations finish. config:cachenever contains stored values. The overlay is skipped whileconfig:cache,optimizeandpackage:discoverrun, so the cached config file holds only.envand config values. At runtime the overlay is applied on top of the cached config, so stored values take effect with or without a config cache, and changing a stored value never requires rebuilding it.
Reading and writing in code
Read through the config. That gives the resolved value whatever its source:
config('services.google.gtm-id'); // stored value, else GTM_ID, else default
config('evolve.company.legal-name'); // stored value, else nullThe settings class gives the raw stored value, null when nothing is stored:
use Racerfish\Evolve\Settings\TrackingSettings;
app(TrackingSettings::class)->gtm_id; // null means "not stored"Writing is ordinary spatie/laravel-settings. The panel is the normal way; on a server without panel access, tinker works too:
php artisan tinker --execute='
$settings = app(Racerfish\Evolve\Settings\TrackingSettings::class);
$settings->gtm_id = "GTM-ABC1234";
$settings->save();
'Set the property to null and save to fall back to the .env again.
Importing existing values
Existing sites carry these values in the .env. The import copies them into the store; it is explicit and per site, never automatic, because it changes where a site takes its values from. A site installed from the 2.19 starter has nothing to import: its .env.example carries no settings, the panel is the only place they are set.
php artisan evolve:settings:import --dry-run # review
php artisan evolve:settings:import # write| Option | Effect |
|---|---|
--dry-run | Prints the table and writes nothing. |
--force | Overwrites stored values that differ from the config. Without it, only properties that are still null are filled. |
--include-defaults | Also imports values equal to the package default (see below). |
--group= | Limits the run to tracking, seo, site, branding, company or mail. An unknown group fails. |
The table lists every bridged property with the columns Setting, Source (the env variable, or the config key where there is none), Config value, Stored value and Action. The actions:
| Action | Meaning |
|---|---|
import | Nothing stored yet, the config value is written. |
overwrite | Only with --force: the stored value differs and is replaced. |
skip (no config value) | The config has no value to import. |
skip (package default) | The value equals the package default, see below. |
skip (same) | The stored value already equals the config value. |
skip (stored, use --force) | A different value is stored and --force was not given. |
Behaviour worth knowing:
- Package defaults stay out of the store. A value equal to what the shipped config resolves with no env variable set is skipped, also under
--force.TRACKING_CONSENT_POLICY=opt-out,TRACKING_SERVER_SIDE=true,APP_NAME=Laravel, the default panel colour and Laravel'shello@example.com/Examplesender are typical. Those keep following the package when a future release changes the default.--include-defaultspins them anyway. - It reads the config before the overlay, so a stored value is never imported over itself.
- Types are cast to the property:
APP_LOGIN_IMAGE_ID=12becomes the integer12,TRACKING_SERVER_SIDE=falsethe booleanfalse. - It is idempotent. A second run finds nothing to do.
- It never edits the
.env. The env values remain the fallback for anything set back tonull. - It is logged. Every run that changes something writes one
settings-importedentry to the activity log with the new values, the old values and whether--forcewas used. - It fails with a hint when
evolve_settingsdoes not exist yet: runphp artisan migratefirst.
Health check
The tracking and mail blocks of /api/health-check carry a sources field that says where each value comes from: settings (the store), config (the .env or a config file) or null (unset).
"tracking": {
"gtm_id": "GTM-ABC1234",
"consent_policy": "opt-out",
...
"sources": {
"gtm_id": "settings",
"ga_measurement_id": "config",
"meta_pixel_id": null,
"pinterest_tag_id": null,
"linkedin_partner_id": null,
"hotjar_site_id": null,
"consent_policy": "config",
"cookie_banner": "settings",
"consent_via_cmp": null,
"server_side": "config"
}
}The mail block lists from_address, from_name, reply_to_address, notification_address and alert_address with the same sources map (2.18). The meta of the tracking and mailer health checks names the source of each value too (gtm_id_source, notification_address_source and so on).
The Shadowed Config health check lists config keys a project config file empties. A shadowed key that a stored setting resolves is reported under resolved_by_settings in the check's meta and no longer warns. See Health Checks.
The settings panel
Since 2.18 the panel writes the store for every group on this page: each field shows the stored value with the inherited .env value as placeholder, an empty field stores null, and switches are selects with an inherit option. Details on Settings. The former variables (APP_NAME, GTM_ID, MAIL_FROM_ADDRESS and the rest of the bridge) are no longer written by the panel; an outdated copy of the settings view that still posts them has them dropped and logged. The .env lines stay where they are and remain the fallback.
The .env part of the screen keeps two fixes from 2.12.
The config cache survives a save (#222). Deployed sites run on a cached config. Saving settings used to clear it and leave the site uncached until the next deploy. Now a save checks whether a config cache exists: without one it runs config:clear as before, with one it rebuilds it by running php artisan config:cache in a separate process (60 second timeout). If the rebuild fails (non-zero exit, no PHP CLI found, proc_open disabled), the cache is cleared so the new values take effect, the reason is logged at error, and the panel shows an error next to the success message: "Settings saved, but the configuration cache could not be rebuilt. The site works with the new values but runs uncached until the next deploy." A save without changes touches neither the .env nor the cache.
To verify on a server: save a setting, then ls bootstrap/cache/config.php still exists and php artisan about --only=cache reports the config as cached.
The queue health check has three states (#225). The Health tab shows it as a select:
| Option | Writes | Effect |
|---|---|---|
| Auto (on/off, queue driver X) | removes HEALTH_CHECK_QUEUE from the .env | The check runs when the queue driver is not sync. The label shows what Auto currently resolves to. |
| On | HEALTH_CHECK_QUEUE=true | Always runs. |
| Off | HEALTH_CHECK_QUEUE=false | Never runs. |
Before 2.12 the first save pinned the value to on or off and Auto could not be restored. An empty HEALTH_CHECK_QUEUE= written by hand now also means Auto.
Pitfalls
- Editing the
.envafter an import. Once a value is stored, changing the variable in the.envhas no visible effect. Change it in the panel, or empty the field there so the.envapplies again. (Before 2.18 the panel itself wrote the.envand ran into this.) - Several servers, one database. The cached map lives in the application cache. With a per-server cache store (the file driver), a save on one server forgets the entry only there; the others keep the old values until their cache is cleared. Use a shared cache store (database or redis) for multi-server setups, or clear the cache on every server after a change.
- Changes outside the settings classes. Editing
evolve_settingswith SQL does not forget the cached map. Runphp artisan cache:forget evolve.settings.v1afterwards.evolve:db:copyand the Envoy database tasks forget it on the receiving side themselves (since 2.12.1). A deploy without--keep-cachesclears the application cache anyway (optimize:clear). - Database copies carry the store. Tracking IDs stored on live arrive on staging with a database copy. That is harmless for tracking, which only emits on live unless
TRACKING_EMITsays otherwise, but check branding and company values on the copy.
Verifying on a server
php artisan evolve:settings:import --dry-run # what is stored, what would be imported
php artisan tinker --execute='dump(config("services.google.gtm-id"))'
curl -s -H "Authorization: Bearer $TOKEN" https://www.example.ch/api/health-check | jq .tracking.sourcesThe dry run's Stored value column shows every stored value; sources in the health check shows which tracking and mail values the store currently supplies.