one-strive/nova-global-filter
A Laravel Nova package that adds a persistent global filter bar to your admin panel. Apply date ranges or custom filters once and keep them active as you move between resources and pages, so you can browse and manage related data consistently without re-filtering every screen.
This is a single-maintainer Nova package (1 star, 0 dependents) that adds a filter layer applied across resources rather than per-resource. Treat adoption as "evaluating a small piece of vendor code you will likely own," not "installing a community standard."
1. Verify compatibility before anything else. Nova's filter internals have shifted between v4 and v5, and this package's release tag is dated 2026-08-05 — confirm the tag actually matches the Nova major you run.
composer show one-strive/nova-global-filter --all
composer require one-strive/nova-global-filter:^1.0
If it is not on Packagist, or the tag looks orphaned, pull it in explicitly and pin the commit:
"repositories": [{ "type": "vcs", "url": "https://github.com/OneStrive/nova-global-filter" }],
"require": { "one-strive/nova-global-filter": "dev-main#<sha>" }
2. Read the source before the README. With a package this small, src/ is the real documentation. Look in this order:
Nova::filters, Nova::resources, menu bootstrapping);apply(Request $request, $query, $value) is implemented and whether it mutates the builder or wraps it;NovaRequest macro — this is where global state is usually smuggled between resources.3. Wire it up in app/Providers/NovaServiceProvider.php. The registration surface is almost always in boot():
public function boot(): void
{
parent::boot();
Nova::globalFilters([
\App\Nova\Filters\CurrentTenant::class,
\App\Nova\Filters\ActivePeriod::class,
]);
}
4. First use case: tenant scoping. The canonical reason to want a global filter is a tenant_id / team_id constraint that must apply to every resource index without editing thirty filters() methods. Start there — it exercises the two things that break first: query-string persistence across resource navigation, and resources whose models lack the column.
5. Smoke test the request cycle. Load two different resource indexes with the filter set, confirm the filter survives navigation, then inspect the JSON payload in the network tab (filters, perPage, page) and confirm the applied SQL via DB::listen or Telescope. Do not trust the UI alone — a global filter that renders but silently no-ops is the common failure.
Filter class contract. You are still writing standard Nova filters; the package only changes where they get attached. Keep apply() narrow and defensive:
class CurrentTenant extends Filter
{
public function apply(Request $request, $query, $value)
{
return $query->where($query->getModel()->getTable().'.tenant_id', $value);
}
public function options(Request $request)
{
return Tenant::pluck('id', 'name')->all();
}
}
Tenant/time-window as the two workhorses. Global filters pay off for (a) multi-tenancy and (b) reporting periods. Both are cross-cutting; both are the ones users expect to "stick" as they click between resources. Anything resource-specific belongs in that resource's filters(), not here.
Namespace your query parameters. Global filters share the same filters payload key as resource filters. If you need two global filters to coexist without clobbering each other, give each a distinct key()/name() and verify the decoding path in the package's request handling — Nova base64/JSON-encodes filter payloads, and a global implementation that decodes without the resource namespace will collide with a resource filter of the same class.
Wrap, don't inherit, for swap-ability. Because the package is a 1-star dependency, put one thin class of your own between it and the app:
abstract class GlobalFilter extends \OneStrive\NovaGlobalFilter\Filter {}
Every filter extends your class. When the package goes stale or you replace it with a dozen lines in your service provider, you change one file.
Compose with lenses, not against them. Lenses build their own query. Decide explicitly whether the global filter applies inside lenses (usually yes for tenant, no for period) and enforce it in the filter rather than relying on the package default.
Persist via query string, not session. Prefer filters that round-trip in the URL so a filtered index is shareable and back-button-correct. Session-persisted globals are the source of the classic "why am I seeing three records" support ticket.
Testing pattern. Nova's index endpoints are testable; assert on results, not on rendered HTML:
$this->actingAs($admin)
->getJson('/nova-api/users?filters='.base64_encode(json_encode([...])))
->assertJsonCount(2, 'resources');
Keep a test that loads every registered resource with each global filter active — that is the regression net for the "column doesn't exist on this model" class of bug.
Column existence is not guaranteed. A global filter runs against models that never anticipated it. Guard by table:
$table = $query->getModel()->getTable();
if (! Schema::hasColumn($table, 'tenant_id')) {
return $query;
}
Cache the schema check per request; hasColumn in a hot index loop is a silent performance tax.
Respect per-resource policy. Nova's canSee / authorizedToViewAny runs per resource. A global filter that exposes tenant names or period options can leak data in a dropdown before any policy check fires. Gate options() on the same authorization as the resource.
Global means all resources, including the ones you forgot. Pivot-less models, audit tables, nova-* package resources. Audit the full Nova::availableResources() list after enabling, not just the three you were thinking about.
Filter state leaking between resources. If the filter appears to apply on Users but not on Posts, the value is being dropped or mis-namespaced in the redirect between resources. Check whether the package rehydrates from the query string or from a cached "last filter" — the latter breaks deep links and shared URLs.
Interaction with withCount / aggregates. Applying a where on a joined relation changes withCount semantics. Test dashboards and metric cards separately from index tables; metric endpoints often bypass Nova's filter pipeline entirely and will show unfiltered totals next to a filtered table.
Soft deletes and global scopes. A global filter that re-queries the model for options() inherits your global scopes — sometimes what you want, often not when the admin needs to filter to trashed records.
Debugging recipe. Log the final SQL rather than guessing: DB::listen() in a service provider, or ->dd() on the builder inside apply() in a scratch branch. Confirm the filter is in the payload and present in the where clause; those two failures have completely different fixes.
Own the upgrade path. Zero dependents means no one else will hit the Nova 6 incompatibility first. Pin the version, watch the repo for six months, and keep the abstraction class from the patterns section so a fork is a one-file change. If the package stalls, the standard replacement is ~20 lines in NovaServiceProvider::boot() pushing a shared filter class into each resource's filters() via a base resource class — write that fallback in a comment next to the require line so future-you knows the exit.
Extension points to check before forking. Confirm whether the package publishes config (php artisan vendor:publish --tag=...), exposes a base class, or binds anything in the container. Publishing the config is cheap insurance even if you change nothing — it documents the defaults you are relying on.
How can I help you explore Laravel packages today?