There's a tension every app hits eventually: some configuration really should live in .env and config/, and some of it the customer wants to change themselves from an admin screen without a redeploy. Timezone. Whether public registration is open. The site name. Today I wired exactly that into kickoff — DB-backed, admin-editable settings — without forcing the rest of the codebase to learn a new way to read config. Everything downstream still calls plain config('app.timezone'). The settings just become the config at boot.

The design goal: one read path

The trap with "settings in the database" is that you end up with two ways to read the same value — config('app.timezone') in some places, app(GeneralSettings::class)->timezone in others — and now every developer has to remember which is authoritative. That's how you get a timezone that's correct in one half of the app and wrong in the other.

So the rule I set: the database is the source of truth, but config() stays the single read path. Settings get loaded once per request and laid over config. Controllers, Blade views, Fortify, third-party packages — none of them know the value came from the database. They read config() like always.

The storage layer is spatie/laravel-settings. Each settings group is a typed class: