backpack/settings
Backpack Settings lets you store and manage app-wide settings in the database, editable through the Backpack admin panel. Define typed key-value settings, group them in the UI, and fetch them anywhere via a simple helper, with no config files or redeploys needed.
backpack/settings is a thin layer on top of Backpack CRUD that gives you a database-backed key/value settings store with an admin UI. You get a settings table, a Setting model, a setting() helper, and a ready-made CRUD controller you extend.
Install it alongside an existing Backpack install:
composer require backpack/settings
php artisan migrate
The package's service provider auto-loads a migration that creates the settings table (key, name, description, value, field, options, timestamps). There is no separate publish step for the migration in current versions, but do run php artisan vendor:publish if the provider shows up in artisan vendor:publish --list — some versions gate the migration behind a tag.
Next, expose the CRUD in routes/backpack/custom.php:
// routes/backpack/custom.php
Route::crud('setting', \App\Http\Controllers\Admin\SettingCrudController::class);
Create that controller by extending the package's:
// app/Http/Controllers/Admin/SettingCrudController.php
namespace App\Http\Controllers\Admin;
use Backpack\Settings\app\Http\Controllers\SettingCrudController as BaseSettingCrudController;
class SettingCrudController extends BaseSettingCrudController
{
// override setupListOperation / setupCreateOperation to reorder fields,
// add columns, or restrict fields per environment
}
Log into /admin/setting, create your first row — e.g. key site_name, field text, value Acme — then read it anywhere:
// In a controller, Blade view, or class
$name = setting('site_name');
<title>{{ setting('site_name', 'Default Title') }}</title>
That's the first use case: a handful of site-wide strings (name, tagline, support email, social URLs) that a non-technical admin can edit without a deploy.
Where to look first: vendor/backpack/settings/src/app/Models/Setting.php (the accessor/caching logic), src/app/Http/Controllers/SettingCrudController.php (the field definitions and which input types are supported), and src/helpers.php (the setting() helper signature). Those three files are the whole package.
Grouping settings. The package has no concept of groups, so use dot-notation keys as a convention: mail.from_address, mail.from_name, social.twitter, social.facebook. To show them grouped in the admin, override setupListOperation() and add a key column plus a filter, or create multiple CRUD routes backed by separate controllers that each addClause('where', 'key', 'like', 'mail.%') in setupListOperation().
Declaring allowed field types. The field column maps to a Backpack field type (text, textarea, number, email, checkbox, select_from_array, etc.). For selects, the options column holds a JSON map:
{"smtp": "SMTP", "mailgun": "Mailgun", "ses": "Amazon SES"}
Then in a service provider or config file, read the choice:
$driver = setting('mail.driver', 'smtp');
config(['mail.default' => $driver]);
Do this in a boot() method of AppServiceProvider, not in a config file — config files are loaded before the DB is guaranteed available (see gotchas).
Reading in Blade and controllers. The helper accepts a default, which is important because the settings table may be empty during first deploy or tests:
setting('contact.email', config('mail.from.address'))
Writing programmatically. Use the model rather than the helper for writes so you can set metadata and invalidate the cache:
use Backpack\Settings\app\Models\Setting;
Setting::set('onboarding.enabled', '1');
// or create the row so it also appears in the admin UI
Setting::firstOrCreate(
['key' => 'onboarding.enabled'],
['name' => 'Onboarding enabled', 'field' => 'checkbox', 'value' => '1']
);
After programmatic writes, clear the package's in-memory/cache layer (Setting::clearCache() — check the version's actual method name; older versions only cache per-request). Seeders that touch settings should call it at the end.
Consuming settings in a settings-aware service. Rather than sprinkling setting() calls, wrap them:
class SiteSettings
{
public function name(): string { return setting('site_name', config('app.name')); }
public function supportEmail(): string { return setting('support_email', config('mail.from.address')); }
public function isOnboardingEnabled(): bool { return (bool) setting('onboarding.enabled', false); }
}
Bind it as a singleton and inject where needed. This gives you type coercion, defaults, and a single place to grep.
Feature flags. The package works fine as a crude feature-flag store: feature.new_checkout as a checkbox, read through SiteSettings::flags(). Don't use it for anything that needs per-user targeting — it's global only.
Restricting access. Wire the setting CRUD route into Backpack's permission manager (backpack/permissionmanager) so only super-admins can edit. Otherwise the route is visible to any role with CRUD access, which is usually too broad.
Wiring settings into Backpack itself. Common wins: override config/backpack/base.php site_name/site_logo from settings on boot, or swap the admin's default mail sender. Keep the boot() call wrapped in a try/catch (or Schema::hasTable) so artisan migrate on a fresh DB doesn't explode.
The helper loads everything at once. On first call, Setting typically selects all rows and caches them for the request. That's fine for dozens of settings and terrible for thousands. If you're tempted to store per-entity data here, stop — it's an application settings table, not a config service.
Cache invalidation is manual. Edits made through the CRUD controller clear the cache, but seeder/updateOrCreate-style writes often don't. If a setting "won't change" in a long-running queue worker or Octane process, call Setting::clearCache() (or the version's equivalent) after writing, and flush on deploy.
Everything is a string. value is stored as text (or JSON depending on version/field). (bool) '0' is false, but (bool) 'false' is true. Cast explicitly based on field type, or do the casting in your wrapper class. Checkboxes come back as "1" / "0" / null, not real booleans.
Config files can't safely call setting(). config/*.php files load before the DB connection is guaranteed. Use AppServiceProvider::boot() and config(['...' => setting('...')]), guarded with Schema::hasTable('settings') if you want to survive fresh installs and CI runs against an empty DB.
Keys are not enforced unique by convention alone. The migration in some versions doesn't add a unique index on key. Two admins can create site_name twice, and you'll get whichever row the query returns first. Add a unique index in your own migration if you care, and use a text field's validation rules in the CRUD to prevent duplicates.
Version matching matters. This package tracks Backpack major versions tightly. Backpack 4/5/6 each have their own backpack/settings releases, and the model's casting/caching behavior has changed between them. Pin the matching version and read the README for that tag — code samples from blog posts are frequently for a different major.
The options column expects JSON, not PHP array syntax. If your select is empty in the UI, it's almost always because options contains ['a' => 'b'] written as PHP rather than valid JSON. Also confirm the field value exactly matches the Backpack field type (select_from_array, not select).
No multi-tenancy, no environments. There's one settings table, period. If you need per-tenant or per-environment values, either add a tenant_id column and a custom model overriding the query, or namespace keys by tenant and accept the admin UI pain. For staging vs production, prefer separate databases — don't try to encode environment in keys.
Extending safely. The intended extension points are: (1) subclass SettingCrudController, (2) subclass Setting and rebind it in a service provider if you need extra columns or casts, (3) override the setting() helper is not clean — prefer your own SiteSettings facade instead so package upgrades don't break callers.
Debugging tip. When a value doesn't appear where expected, run Setting::all()->pluck('value', 'key') in php artisan tinker — if the value is there but not in the app, it's a cache problem; if it's not there, it's a CRUD/validation problem. Those are the only two failure modes in practice.
How can I help you explore Laravel packages today?