cwsps154/app-settings
Filament App Settings lets you build a customizable settings panel with all Filament form components, add custom tabs via an artisan command, and control page and tab access with simple plugin methods. Includes install command and publishable config.
The package is a Filament panel plugin that gives you a ready-made "App Settings" page backed by custom Filament forms, so you don't hand-roll a settings resource. It is small (single maintainer, low star count), so treat the source as the documentation after the first install.
Minimal path:
composer require cwsps154/app-settings
php artisan app-settings:install
php artisan migrate
Look for the settings table (typically app_settings) and any seeded default rows.panel() method of your Filament PanelProvider:
use CWSPS154\AppSettings\AppSettingsPlugin; // verify actual namespace in vendor/
$panel->plugins([
AppSettingsPlugin::make(),
]);
Where to look first, in order:
vendor/cwsps154/app-settings/src/AppSettingsPlugin.php — the plugin entry point and the canAccess, canAccessAppSectionTab, and appAdditionalField methods.config/app-settings.php or similar) — the real extension surface.AppSettings model and its migration — how values are stored (key/value vs. one column per field matters a lot).If you only need two or three global flags, this package is still worth it over a custom page, because you inherit Filament's form components (toggles, file uploads, rich editors) and the permission hooks for free.
Gate the page per panel or role. The access callbacks are the main integration point. Wrap your own policy/role checks:
AppSettingsPlugin::make()
->canAccess(fn () => auth()->user()?->can('manage app settings') ?? false)
->canAccessAppSectionTab(fn () => auth()->user()?->hasRole('admin') ?? false);
If you use cwsps154/users-roles-permissions, plug its helpers straight into these callbacks rather than duplicating logic.
Add your own settings sections. Use appAdditionalField() to inject extra sections into the App tab. Keep each section as a Filament Section with a stable, prefixed field name so you can retrieve values predictably:
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
AppSettingsPlugin::make()
->appAdditionalField([
Section::make('Branding')
->schema([
TextInput::make('branding.site_name')->required(),
TextInput::make('branding.support_email')->email(),
]),
]);
Read settings through the model, not the form. The page is an editor; your app code should consume settings via the AppSettings model (or a small Settings facade/wrapper you write). That wrapper is worth the ten minutes:
class Settings
{
public static function get(string $key, mixed $default = null): mixed
{
return cache()->rememberForever("setting.{$key}", fn () =>
AppSettings::query()->where('key', $key)->value('value') ?? $default
);
}
}
Treat the settings page as config, not user data. It's a write-rarely, read-often surface. Cache reads aggressively and bust the cache in an observer or in whatever callback runs after the settings form saves.
Layer environment-specific defaults. Store the database value as an override and keep a config/app-settings.php (or your own config) as the fallback, so a fresh environment works before anyone opens the panel:
public static function get(string $key): mixed
{
return AppSettings::query()->where('key', $key)->value('value')
?? config("app-settings.defaults.{$key}");
}
Multi-panel / multi-tenant. If several panels need different settings, register the plugin per panel and scope your reads by tenant (or store the panel/tenant id as part of the key). Don't assume one global row set.
Testing. Because the page is pure Filament, livewire() tests against the settings page cover save + validation. Add a unit test around your Settings::get wrapper for cache-hit and default-fallback behavior — that's where real bugs live.
The README's namespaces are inconsistent. It shows CWSPS154\UsersRolesPermissions\AppSettingsPlugin in the usage example and then references CWSPS154\AppSettings\Models\AppSettings. The plugin likely lives under CWSPS154\AppSettings. Verify with:
grep -R "class AppSettingsPlugin" vendor/cwsps154/app-settings/src
Copy the real namespace into your provider; don't paste the README snippet blindly.
Pin the version. Nine stars, one maintainer, no dependents. Use an explicit constraint in composer.json (e.g. "cwsps154/app-settings": "^1.0") and commit composer.lock. Review the published migration before running it in production — small packages sometimes ship opinionated column choices you'll want to adjust (or a destructive rollback).
Cache invalidation is on you. If you add caching in a wrapper, remember to forget the key on save. A stale settings cache is the most common "the toggle didn't work" bug report.
File uploads need a disk. Any image/file field you add via appAdditionalField() writes through Filament's storage. Set an explicit disk in the field and confirm php artisan storage:link has run, or images will 404 in the panel.
canAccessAppSectionTab gates the tab, not the field values. It hides the UI; it does not protect stored settings from being read elsewhere. If a setting is sensitive (API keys, SMTP passwords), consider encrypting at rest with a model cast rather than relying on the panel gate.
The installer is not idempotent-safe in all cases. If you re-run php artisan app-settings:install after editing published files, check whether it overwrites config or re-seeds settings. Back up the published config before re-running, and prefer copying changes manually.
appAdditionalField() expects Filament schema components. Passing a raw array of fields without a wrapping Section may render but will look ungrouped, and layout components (Grid, Tabs) can behave unexpectedly if they aren't the expected shape. When in doubt, inspect how the default App tab is built in the package source and mirror that.
Debugging saves: if a field silently doesn't persist, confirm the field's name matches the key your read path queries, and that the field isn't missing a dehydrated() / is inside a disabled section. Filament's ->debugMode() on the form and Log::info($state) in a save hook are the fastest way to see what's actually being written.
Extension points summary: plugin fluent methods (canAccess, canAccessAppSectionTab, appAdditionalField), the published config, the AppSettings model (add accessors/encryption casts there), and Filament's own form component API. If you need a whole new tab or a second page, you're outside the package's scope — build a normal Filament page and use this package only for shared global config.
Exit strategy: because the storage is a plain table, migrating off this package is easy — point your Settings wrapper at the table directly and drop the plugin registration. Keep that wrapper in your own code from day one so the package stays a UI convenience rather than an architectural dependency.
How can I help you explore Laravel packages today?