liberusoftware/settings
Liberusoftware Settings is a Laravel package for managing application settings through a simple, fluent API. Store, retrieve, and organize configuration values or user preferences in the database, with support for defaults and easy integration into any Laravel project.
Install and publish
composer require liberusoftware/settings
php artisan vendor:publish --provider="Liberu\Settings\SettingsServiceProvider"
php artisan migrate
Pin the version ("liberusoftware/settings": "^1.0") before you install. With zero stars and zero dependents there is no community buffer catching breaking changes for you, so treat the lockfile as load-bearing.
Where to look first, in this order
config/settings.php — this is the package's real API surface. It tells you the resolution hierarchy (application → organization → team/site → user), the cache store and TTL, and the table/column names. Read it before writing any code.organization_id, team_id, user_id) and whether they're nullable, because "application-level default" is usually modeled as a row with all scope columns null. That nullability determines your unique index and therefore your upsert semantics.First use case
Start with a single low-stakes, user-overridable preference rather than a critical app setting. Something like a notification cadence:
// Define it once, typed and validated
Settings::define('notifications.digest', 'string', default: 'daily', rules: ['in:daily,weekly,never']);
// App-level default lives in a seeder
Settings::forApplication()->set('notifications.digest', 'daily');
// User override
Settings::forUser($user)->set('notifications.digest', 'weekly');
// Read — resolution walks user → team → org → application
$cadence = Settings::forUser($user)->get('notifications.digest');
That exercise exercises every part of the package: definition, typing, validation, scoped write, cascade read, and cache invalidation. If one of those four steps doesn't behave, you've learned it before it's load-bearing.
Define the schema centrally, write through it, never around it
Treat the setting definitions as a contract and put them in one place (a service provider or a dedicated SettingsSchema class) so the type, default, and validation rules can't drift. The whole value proposition over config() or a hand-rolled settings table is that a write is validated and typed at the boundary. The moment someone does DB::table('settings')->insert(...) to "quickly fix something in prod," you've lost type safety and the cache is stale in a way nobody will debug well.
Explicit scope in the call site, not ambient auth
The tempting API is Settings::get('foo') magically resolving from auth()->user(). Use the explicit forUser($user) / forTeam($team) form everywhere instead. It reads better, it's testable without faking auth, and it survives the moment you need to read another user's settings from an admin panel or a job.
Read-through resolution with explicit fallback
Model the cascade as a first-class concept in your own code:
$cadence = Settings::forUser($user)->get('notifications.digest')
?? Settings::forTeam($team)->get('notifications.digest')
?? Settings::forApplication()->get('notifications.digest')
?? config('settings.defaults.notifications.digest');
Whether the package does this for you depends on the resolver — check the source. If it does, use it; if it half-does it (e.g. skips the organization tier for some keys), wrap it once in your own resolver class rather than repeating the fallback chain in twenty controllers.
Validation reuse in form requests
If definitions carry Laravel rule strings, pull them into your FormRequest rather than restating them:
public function rules(): array
{
return [
'notifications.digest' => array_merge(
['sometimes'],
Settings::rulesFor('notifications.digest')
),
];
}
One source of truth means the settings UI and the API can't disagree about what's valid.
Bulk writes for settings screens
Admin settings pages submit a whole form at once. Look for a setMany() or similar on the scope object — if it doesn't exist, write a small transaction wrapper in your app layer that loops set() calls. Do not loop set() calls in a controller outside a transaction: partial failure leaves you with half-applied settings and an invalidation event for each key.
Exposing settings to the frontend
Resolve once per request and share a single array, not one call per field:
Inertia::share('settings', Settings::forUser($user)->allResolved());
allResolved() (or a manual loop over your known keys) beats N individual cache lookups, especially if the package stores each key as its own cache entry.
Testing
array cache store in phpunit.xml so tests don't leak state between cases.RefreshDatabase plus a settings seeder; if the package caches aggressively, add a Settings::flush() (or equivalent) to setUp().'yes' to true when you asked for integer is a bug you want pinned by a test.The cache is the whole ballgame. "Deterministic cache invalidation" is the selling point, so verify it under your actual deployment topology. If you run multiple app servers or queue workers, the cache store must be shared (Redis/memcached/database) — a file cache store means invalidation on one node leaves every other node serving stale settings until TTL expiry. Check config/settings.php for the cache store key and make sure it isn't defaulting to the app's default store if that's file in production.
Queue workers and CLI have no auth context. Any code path that relies on ambient resolution (auth()->user()) will resolve to the application tier — or blow up — inside a job, a scheduled command, or artisan tinker. Pass the scope objects explicitly into jobs via serialized IDs and re-hydrate them in the job's handle(). This is the single most common way scoped-settings packages cause production incidents.
Type coercion surprises. Form input, .env values, and JSON payloads all arrive as strings. A boolean setting written as "false" is truthy in PHP. Either always write through the validated API (so "false" becomes false), or check whether the package casts on read vs. on write — the two are not the same thing and mixing them produces keys that read back correctly only in production.
Migration and index details. Check the unique constraint. If it's unique(key) you can't have per-user overrides at all; if it's unique(key, organization_id, team_id, user_id) you need to be aware that MySQL and Postgres treat NULL differently in unique indexes — multiple application-level rows for the same key may be allowed on one and rejected on the other. Seed defaults idempotently with updateOrCreate-style semantics.
Don't put secrets here. Settings are typically cached, may be dumped into the frontend, and may be readable via admin UI. API keys and credentials belong in config/ + a secrets manager. Use this package for behavior toggles, thresholds, branding, and preferences.
Version pinning and source reading. Zero stars, zero dependents, and an unusual release date mean the docs are thin and the test suite is your real spec. Before adopting, spend twenty minutes in src/: read the resolver, the cache invalidation path, and the validation entry point. If any of those three is missing or under-tested, budget for a thin wrapper in your app layer so you can swap out the package later without touching call sites. Isolating it behind a SettingsRepository of your own is cheap insurance.
Extension points worth knowing. Typical hooks in packages like this: a custom cache key strategy (useful if you run database-per-tenant and want namespace isolation), a custom resolver (to add a tier the package doesn't ship, e.g. "client" between org and team), and custom cast/validator classes registered per type. Confirm which exist before you fork — a resolver override is usually a one-line binding, whereas forking means you own the maintenance.
Watch the invalidation on deletes and scope changes. Writing a setting is the obvious case; the interesting one is what happens when a user moves teams or an organization is renamed/deleted. If cached resolved values are keyed by scope ID, changing a user's team may leave them reading the old team's settings until TTL. Test that path explicitly — it's the kind of bug that surfaces three weeks later as "my preferences keep resetting."
How can I help you explore Laravel packages today?