Confirm compatibility before you install anything. The last tagged release is from November 2019, which predates Nova 4 entirely and sits at the tail end of the Nova 2.x era. Before running composer require:
# Peek at the constraint without installing
composer show ericlagarda/nova-settings-card --all | grep -i nova
If the constraint is ^2.0, you are on a package that will not resolve against a modern laravel/nova. In that case, treat the repo as source material to vendor or fork rather than a dependency.
Install path (Nova 2.x era projects only):
composer require ericlagarda/nova-settings-card
php artisan vendor:publish --provider="EricLagarda\NovaSettingsCard\CardServiceProvider"
Publishing typically drops a config file, a migration for the settings table, and the compiled card assets. Run php artisan migrate and check config/nova-settings-card.php for the table name and field definitions.
Register the card in app/Providers/NovaServiceProvider.php:
public function cards()
{
return [
new \EricLagarda\NovaSettingsCard\SettingsCard(),
// or with options, e.g. a key to scope the settings group
(new \EricLagarda\NovaSettingsCard\SettingsCard())->settingsKey('general'),
];
}
Where to look first: open src/ and read the Card class and its controller/route. The card is a thin Nova wrapper, so the real behavior lives in two places — the Vue component under resources/js/ (what fields render) and the controller that persists the payload. Read both before wiring anything into app code; the persistence format (a JSON blob in one row vs. key/value rows) determines how you read the settings back.
First use case: a single dashboard card holding site-wide, non-secret values — site name, support email, footer text, a maintenance-mode toggle. Anything an admin should change without a deploy. Do not start with mail credentials or API keys; see Gotchas.
Read settings through a cached accessor, never from the card. The card's job is editing; your application should never touch the card class. Wrap reads in one place:
namespace App\Support;
use Illuminate\Support\Facades\Cache;
class Settings
{
public static function get(string $key, $default = null)
{
return Cache::rememberForever("settings.{$key}", function () use ($key, $default) {
return \DB::table(config('nova-settings-card.table', 'settings'))
->where('key', $key)
->value('value') ?? $default;
});
}
}
Adjust the query to match whatever schema the published migration actually creates — this is the one place you must read the source rather than assume. Then flush on save. If the package fires an event or hits its own controller, hook your cache-clear there; if not, override the controller or add an observer on the settings table.
Scope the card per settings group. If the card accepts a scope argument, register several instances instead of one giant form:
public function cards()
{
return [
(new SettingsCard())->settingsKey('general')->width('1/2'),
(new SettingsCard())->settingsKey('mail')->width('1/2'),
(new SettingsCard())->settingsKey('features')->onlyOnDetail(),
];
}
This keeps the payloads small and lets you use canSee() for per-group authorization:
(new SettingsCard())->settingsKey('mail')
->canSee(fn () => Gate::allows('manage-mail-settings')),
Put the card on a dedicated tool/dashboard, not the main dashboard. Settings are edited rarely. Either register a separate Nova::dashboard() or attach it to a resource detail page (->onlyOnDetail()) so it doesn't compete with daily metrics. A common pattern is one "Settings" dashboard registered via Nova::dashboards() and linked from the sidebar.
Pair it with a typed layer for feature flags. Reading a string out of the card and casting in twenty call sites is where this gets messy:
class Feature
{
public static function enabled(string $flag): bool
{
return filter_var(Settings::get("features.{$flag}", false), FILTER_VALIDATE_BOOLEAN);
}
}
Local development loop for the Vue side. The card's front end lives under the package's resources/js. Published assets are stale by definition; if you need to change a field type or layout, work in a forked copy inside nova-components/NovaSettingsCard and run npm install && npm run prod there. Nova loads the compiled dist/js/card.js from public/vendor/.
Testing: assert on your Settings accessor, not on the card. For Nova-side behavior, use Laravel\Nova\Testing\ helpers (Nova 4) or a simple $this->actingAs($admin)->get('/nova-api/...') smoke test against the card's route. Do not write fine-grained tests against the card's internals — you will rewrite them on fork.
This is effectively unmaintained. Eight stars, zero dependents, one release in 2019. There is no community to catch breakage. Budget for reading and owning the code, or pick an alternative: spatie/laravel-settings (typed settings classes, no Nova UI but trivial to wrap in a custom card), inspheric/nova-settings for a maintained Nova-native equivalent, or a 30-line custom card with nova:card — for a single settings screen, the custom card is often less work than adapting this one.
Nova major-version drift is the primary failure mode. Nova 3 changed card registration and asset publishing; Nova 4 reworked the front end and dropped a number of Vue 2 patterns. Symptoms are a blank card, a console error about an unknown component, or Unable to locate file in Vite manifest. If the card renders nothing, open the browser console first — it is almost always the compiled JS, not PHP.
Asset publishing silently no-ops. vendor:publish without --force will not overwrite existing public/vendor/... assets, so a fork's rebuilt card.js may never load. Delete the vendor directory under public/vendor/ericlagarda (or whatever the tag is) and republish, or run with --force in local dev.
Never store secrets here. The card's persistence is a plain database column, typically rendered back into the Nova UI in cleartext, and often logged by query logging. Mail passwords, API keys, and webhook secrets belong in .env or an encrypted cast. If you must, wrap the value with Crypt::encryptString() in the controller — but that requires forking the save path.
Migration collisions. The published migration will create a generically named table (settings, nova_settings). On an existing app this can clash with an earlier settings implementation. Rename the table in both the migration and the config before running php artisan migrate, and confirm the card reads the table name from config rather than hardcoding it — if it's hardcoded in the controller, that's your first fork patch.
Stale values after save are a caching bug, not a card bug. If edits appear to save but the site doesn't change, your cache layer isn't being invalidated. Add the flush to the save path explicitly. During debugging, Cache::forget() the key by hand to confirm.
Authorization is one line away from being absent. A card with no canSee() is visible to every Nova user, and the underlying endpoint may not check gates either. Verify the controller authorizes the request — a card's visibility is UI, not security. Add the gate check server-side and a Nova policy if you expose the card on a resource.
Extension points are thin, so patch deliberately. The realistic customization points are: the published config (field list/table name), a subclassed card overriding constructor options, and the Vue component. Keep a CHANGELOG-style note in your repo of every change you make to the fork — with no upstream maintenance you will be the only person who knows why line 40 differs.
Debugging checklist, in order: (1) is the card in NovaServiceProvider::cards() and did you clear php artisan nova:cache? (2) browser console for Vue errors; (3) php artisan route:list | grep -i settings to confirm the endpoint exists; (4) hit that endpoint directly with a Nova-authenticated session; (5) re-read the controller — persistence bugs are usually there, not in the JS.
How can I help you explore Laravel packages today?