epigra/nova-settings
Epigra Nova Settings adds a settings management tool to Laravel Nova, letting you create, organize, and edit application settings from a dedicated interface. Store key/value settings in the database and access them easily throughout your Laravel app.
Install the package and publish its assets first — the tool ships with a config file and a migration that are both required before anything renders in Nova.
composer require epigra/nova-settings
php artisan vendor:publish --provider="Epigra\NovaSettings\ToolServiceProvider"
php artisan migrate
Then register the tool in app/Providers/NovaServiceProvider.php:
public function tools()
{
return [
new \Epigra\NovaSettings\NovaSettings,
];
}
Where to look first: open config/nova-settings.php after publishing. That file is the source of truth for which setting groups/tabs appear, their labels, and often the storage/cache behaviour. Read it before writing any code — most "the tool is empty" confusion is a config registration problem, not a code problem.
First use case: site-wide editable values that non-developers need to change without a deploy — support email, social links, a maintenance-mode toggle, footer copy. Define a settings class (or a config entry, depending on your version) that returns native Nova fields:
// app/Nova/Settings/GeneralSettings.php
class GeneralSettings
{
public static function fields(): array
{
return [
Text::make('Support Email', 'support_email')
->rules('required', 'email'),
Boolean::make('Maintenance Mode', 'maintenance_mode'),
Image::make('Logo', 'logo')->disk('public'),
];
}
}
Register it in the config, reload Nova, and you have an editable panel. Read the values back in your app through the package's helper/facade (commonly settings('support_email') or Settings::get('support_email')) — check the package's README for the exact accessor name, since it has changed across releases.
One settings class per domain, not one giant form. Group related values — GeneralSettings, SocialSettings, SeoSettings — and map each to a tab in the config. Nova renders them as separate panes, which keeps the tool navigable as the number of values grows and lets you scope authorization per group later.
Treat the field definitions as the schema. Because fields are native Nova fields, you get the full field API for free: validation ->rules(), ->help(), ->placeholder(), conditional display via ->dependsOn(), and ->readonly(). Lean on that instead of building custom form logic. For a select whose options are dynamic, compute them in fields() rather than hardcoding:
Select::make('Currency', 'currency')
->options(collect(config('app.currencies'))->pluck('name', 'code'))
->default('USD'),
Always define defaults and fall back in the reader. Settings rows may not exist yet on a fresh environment. Wrap reads:
$email = settings('support_email') ?? config('mail.from.address');
Better: centralise this in a small typed accessor class (app/Support/SiteSettings.php) so business logic never hits the raw package helper. That gives you one place to change when the package's API shifts and one place to add casting (JSON → array, "1" → true).
Cache aggressively and invalidate on save. These values are read on nearly every request; a DB hit per key adds up. If the package doesn't cache automatically (check the config — there's usually a cache toggle and TTL), wrap your accessor in Cache::rememberForever() and flush on the tool's save event, or override the model/save hook to call Cache::forget().
Keep secrets out of it. This is a convenience store for human-editable values, not a vault. API keys and tokens belong in .env/your secrets manager. If you must store one, at minimum encrypt it in the accessor and gate the group behind a policy.
Expose it to the frontend deliberately. Don't dump the whole settings payload into every Blade view via a global composer. Pass individual keys from the controller, or add a small SettingsResource endpoint returning only public keys — settings tables drift into accidental data leaks otherwise.
Test with the store, not the UI. In feature tests, seed values by writing directly through the package's model/facade in setUp(), then assert on your app's output. Don't test the Nova tool itself; test the read path your application depends on.
vendor:publish won't overwrite an existing migration or config without --force. If you upgrade the package and a migration is missing, publish with --force into a scratch dir and diff it by hand rather than overwriting your config.title, decide up front whether they're namespaced (general.title vs seo.title) and be consistent — silently overwriting each other's values is easy to miss.false vs missing. A Boolean field that was never saved returns null, not false. Use (bool) settings('maintenance_mode') or the field's ->default(false) plus a null-coalescing read, otherwise a "missing" flag will read as off when you meant "unset" — or worse, as on.Image field leaves the old file on disk. Listen for the save/update event and delete the previous path if orphaned files matter to you. Also confirm the disk is in config/filesystems.php and publicly linked (php artisan storage:link).NovaServiceProvider::tools(); if a custom policy or menu override depends on the settings tool existing, register it after. And remember nova-components/tool routes are cached with Nova — after changing config, run php artisan nova:cache (or clear it) if the panel doesn't update.->canSee(fn ($request) => $request->user()->isAdmin()) in tools(), and push finer-grained control into Nova policies if a group is sensitive (e.g. billing details hidden from editors).'model' => App\Models\Settings::class). Subclass it when you need casts, observers, or a custom get() so you don't fight the package in service providers.Settings::all() or DB::table('settings')->get()) and the raw cache entry. The mismatch is almost always (a) cache, or (b) two reads using different group names.Text fields store one string. Either define one field per locale (title_en, title_fr) — simple and predictable — or wrap the field with a translatable field package. Mixing the two approaches across the app gets messy fast.composer.json and check the package's PHP/Laravel constraints before a framework upgrade. Keep a thin accessor class in front of it so swapping to a plain settings table implementation later is a one-file change rather than a hundred call-site edits.How can I help you explore Laravel packages today?