optimistdigital/nova-settings
Nova Settings adds a dedicated settings tool to Laravel Nova for managing database-backed configuration. Define settings with familiar Nova fields, edit them through a clean UI, and access stored values from your application. A practical way to make app settings editable without code changes or rede...
Install, publish, and migrate:
composer require optimistdigital/nova-settings
php artisan vendor:publish --provider="OptimistDigital\NovaSettings\NovaSettingsServiceProvider"
php artisan migrate
Publishing produces a config file (config/nova-settings.php) and a migration creating the nova_settings table (id, key, value JSON, timestamps).
Register the tool in app/Providers/NovaServiceProvider.php:
public function tools()
{
return [
new \OptimistDigital\NovaSettings\NovaSettings,
];
}
Then declare the fields you want to edit. This is the single most important integration point — fields are registered statically, usually in a dedicated service provider's boot():
use OptimistDigital\NovaSettings\NovaSettings;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\Image;
public function boot()
{
NovaSettings::addSettingsFields([
Text::make('Site name', 'site_name')->default('My App')->rules('required'),
Text::make('Support email', 'support_email')->rules('email'),
Boolean::make('Maintenance mode', 'maintenance_mode'),
Image::make('Logo', 'logo')->disk('public')->path('branding'),
]);
}
Read values anywhere in the app:
$name = nova_get_setting('site_name', 'My App');
$all = nova_get_settings(); // keyed array of every stored value
The first practical use case is almost always brand/marketing/support copy — site name, logo, support email, social URLs, feature toggles — things ops or marketing want to change without a deploy.
Where to look first: the generated config file (table name, can_see/can_update, dependency reload), then the Nova sidebar's "Settings" page to confirm fields render correctly.
Keep registration in one place. Create app/Providers/SettingsServiceProvider.php (or a Settings class with a boot()), and call NovaSettings::addSettingsFields() there rather than scattering calls across packages. This gives you one file to diff when a setting changes and lets you conditionally register fields per environment:
if (app()->isProduction()) {
NovaSettings::addSettingsFields([Boolean::make('Beta access', 'beta')]);
}
Group by domain with panels. The tool renders native Nova fields, so panel grouping keeps a growing settings page usable:
NovaSettings::addSettingsFields([
Panel::make('General', [/* ... */]),
Panel::make('Email', [/* ... */]),
Panel::make('Integrations', [/* ... */]),
]);
Wrap access in a typed helper. Raw nova_get_setting() returns whatever was JSON-decoded from the DB. Centralize keys and defaults so callers never pass string literals:
class Settings
{
public static function siteName(): string
{
return (string) nova_get_setting('site_name', config('app.name'));
}
public static function maintenance(): bool
{
return (bool) nova_get_setting('maintenance_mode', false);
}
}
Then use Settings::maintenance() in middleware, Settings::siteName() in Blade, etc.
Apply settings at boot for config overrides. Common pattern for mail/queue/branding defaults:
public function boot()
{
if (Schema::hasTable('nova_settings')) {
config([
'mail.from.address' => nova_get_setting('support_email', config('mail.from.address')),
'mail.from.name' => nova_get_setting('site_name', config('app.name')),
]);
}
}
Guard with a table-existence check so php artisan migrate on a fresh DB doesn't explode.
Use Nova's richer fields for structured data. Repeater, KeyValue, Select, Markdown, File/Image, and Color all work and store their native JSON structures. This is where the package earns its keep — you get the same editing UX as resources without writing a CRUD resource.
NovaSettings::addSettingsFields([
KeyValue::make('Social links', 'social_links'),
Repeater::make('Footer columns', 'footer_columns')->fields([
Text::make('Heading'),
Textarea::make('Body'),
]),
Markdown::make('Terms of service', 'terms_md'),
]);
nova_get_setting('social_links') then returns an associative array. As of 6.0.2, Markdown fields render Nova's live preview toggle inside the settings form — useful for long-form legal/onboarding copy stored as settings, since non-developers can now preview formatting before saving. Pin >=6.0.2 if you rely on it.
Validate at save time. Any ->rules() on the field is enforced when the settings form is submitted — reuse the same rules you'd put on a resource.
Authorization via config closures. Restrict the whole page rather than individual fields:
'can_see' => fn ($request) => $request->user()?->isAdmin(),
'can_update' => fn ($request) => $request->user()?->can('manage settings'),
Dependency-driven UI. Fields using ->dependsOn() work, and reload_page_on_dependencies_update in config controls whether the page reloads after save so newly visible fields appear. Leave it on unless you have a reason not to.
Seeding for reproducible environments. Since defaults live on the field, nova_get_setting($key, $default) is the source of truth when nothing is stored. For local/staging parity you can alternatively seed the nova_settings table directly.
config:cache. Laravel can't serialize closures, so can_see/can_update closures will fail under php artisan config:cache. Use invokable classes or a gate check instead if you cache config in production.'email' will silently collide — the last write wins. Prefix keys per domain (billing_email, marketing_email). There is no dot-notation nesting on read; store arrays via KeyValue/Repeater and index them yourself.nova_get_setting('feature', true) returns false only if false was explicitly saved. A never-saved key returns true because no row exists. Cast explicitly and pass defaults for every read.->default() renders the initial value but does not persist it. Any code path calling nova_get_setting() without a second argument can still get null before someone opens the settings page and hits Save.Storage::disk($disk)->url($path) at the call site; don't assume the value is a fully qualified URL.site_name → app_name, migrate the data manually or read from both keys during the transition.whereNotIn('key', $registeredKeys)->delete() cleanup is worthwhile once you have dozens of settings.nova_get_setting() in a hot loop or a Blade partial rendered per item, memoize the whole set once: Cache::rememberForever('settings', fn () => nova_get_settings()) and flush on save (or on a short TTL)."tenant_{$id}_name") and manage lifecycle yourself, or reach for spatie/laravel-settings instead.can_see can read API keys stored in a Text field. Prefer .env for credentials; if you must, restrict with can_update/can_see, mark the field ->hideFromIndex()-like via password input type, and audit access.optimistdigital/nova-settings to outl1ne/nova-settings (namespace OptimistDigital\ → Outl1ne\). Fresh installs may resolve either name; check your composer.json and use statements before upgrading, and republish config/migration tags if the namespace changed.RefreshDatabase and register fields in a test service provider; otherwise nova_get_setting() returns nulls and the tool route 404s.nova_settings row directly first, then field rules() (a failed validation silently prevents save), then the config closure (can_update returning false hides the Save button). The DB is the fastest triage point.How can I help you explore Laravel packages today?