novius/laravel-nova-contexts
Adds selectable contexts to Laravel Nova: define scopes such as sites, tenants, or locales, let users switch between them from the Nova UI, and automatically apply the matching global scopes to your resources and queries.
Prerequisites, in order. This package is a Nova presentation layer for novius/laravel-contexts; it does not create the context concept itself. Get the base package working first — migrations run, Context model seeded, middleware registered, context() helper (or whatever accessor the installed version exposes) returning the active context in a normal web request — before installing the Nova package. If the base package is not already scoping your front-end, this package will only make the Nova panel match a broken foundation.
Install and publish.
composer require novius/laravel-nova-contexts
php artisan vendor:publish --provider="Novius\LaravelNovaContexts\LaravelNovaContextsServiceProvider"
Then read the published config/nova-contexts.php (name will vary by version) in full — it is the primary documentation surface for this package. Expect keys along the lines of: the context model class, the column/FK used on contextable resources, the switcher label attribute, and whether resource scoping is applied automatically. With no README of substance and a last release in July 2022, the config file and the src/ tree are your real reference; open vendor/novius/laravel-nova-contexts/src/ and read the service provider before writing any code against it.
Register the tool. The context switcher is a Nova tool/header component. Register it in nova-components/NovaServiceProvider::tools() as instructed by the package's own service provider, then run php artisan nova:publish if the package ships assets. Confirm the switcher appears in the header on a fresh page load before wiring any scoping — that isolates "the package loads" from "the package scopes".
First use case. The canonical one: a Site (or Store, Brand) model with a sites-style table; content resources (Page, Post, Product) carry a site_id FK; the switcher changes the active site; the Nova resource index for Page shows only the current site's pages and new pages inherit it. Pick exactly one resource for the first pass, get it scoping correctly in index, detail, create, and update, then roll the pattern to the rest. Do not migrate ten resources before validating one end-to-end.
Model-side setup. Follow the base novius/laravel-contexts conventions rather than inventing your own: add the contextable trait and the context column (or pivot, if you support many-to-many contexts) to each resource model. The Nova package reads whatever the base package exposes, so an inconsistent trait/column pattern across models is the single most common reason scoping "works for some resources but not others."
Resource scoping. The idiomatic integration point is indexQuery() / detailQuery() on the Nova resource, or a global scope on the model that Nova inherits for free. Prefer the model-level scope if the base package provides one — it then applies to Nova and to your front-end controllers and API, keeping a single definition of "current context." Use NovaRequest-level filtering only where you need an admin escape hatch (see below). Verify the scope survives Nova's eager loads on relations: BelongsTo/HasMany fields do not automatically inherit the parent's scoping in every case.
Auto-assigning context on create. The switcher's job is only to hold the active context; the resource must still stamp it. Two clean options:
fields() returns a BelongsTo context field that is ->default(fn () => context()->id)->readonly() — visible and honest about what's happening.fillUsing/model creating event sets it silently and the field is hidden entirely.Choose (1) if admins ever need to move content between sites; (2) if context is an invariant. Do not mix the two across resources — inconsistent visibility of the context field confuses editors more than either extreme.
Context-aware filters and lenses. Expose the switcher's value as a filter on resources that legitimately span contexts (a "all sites" admin view). Nova lenses and the filters() method receive the underlying query, so a lens that calls ->withoutGlobalScope(ContextScope::class) is your sanctioned cross-context view. Keep these few, named explicitly (AllSitesLens), and document them — an accidental lens that drops the scope is a data-leak-shaped bug in a multi-tenant setup.
Policies and authorization. Scoping filters queries; it does not authorize. If a user should only edit their own site's content, that belongs in a policy (update() checks $user->site_id === $page->site_id), because the switcher is UI state, not a permission. Treat a forged switcher value as hostile input: validate that the posted context is in the user's allowed set in the tool's controller or a middleware, not just in the Vue component.
Non-HTTP execution contexts. Anything without a request — queued jobs, scheduler, artisan commands, Nova actions that dispatch jobs — has no active context. Pass the context id into the job constructor and re-establish it at the top of handle(); do not rely on session state leaking through the queue (it will not, and in some drivers it will silently be the wrong one).
Testing. Nova tests and feature tests both need an explicit context. Set it in setUp() (via the base package's manager/facade, or by actingAs a user whose context is derived from the request) and assert cross-context invisibility: create a record in site A, switch to site B, assert it does not appear in indexQuery()->get(). That single negative test is worth more than the positive one.
Compatibility is the first risk, not a footnote. Last release July 2022, targeting Nova 4 / Laravel 9. Nova 5 and Laravel 10+ changed enough (Nova's tool registration and header asset pipeline in particular) that you should assume breakage until proven. Pin the version in composer.json, install into a branch, and smoke-test the switcher's Vue assets under your Nova version before committing to the approach. If the front-end component fails to compile against Nova's current mixin set, forking the header component is usually cheaper than finding an alternative package — the PHP-side scoping is the hard part, the switcher is a <select>.
Scoping leaks through Nova internals. Search, metrics, cards, and relationship fields each build their own queries. A global scope on the model catches most of them; a indexQuery()-only implementation catches almost none. Test search results and every metric on the dashboard with two seeded contexts. If a metric ignores the scope, it is aggregating across sites — a reporting bug at best, a disclosure at worst.
Cache keys must include the context id. Any cached resource list, dashboard value, or fragment keyed only on user/tenant will serve site A's data to site B after a switch. Sweep your cache keys as part of the rollout, and check whether the base package's repository/cache layer already does this.
The switcher persists in session, which means it is per-browser and stale. Two tabs with different contexts is undefined behaviour; a shared workstation keeps the last user's context. Decide deliberately whether the switcher survives logout (usually it should reset) and how it interacts with a per-user default context. Clear the key on Logout and on user impersonation (Nova's impersonate feature) — otherwise impersonating an admin silently inherits the impersonator's context.
Middleware ordering matters. Context resolution must run before Nova's resource resolving and after auth if your context derivation depends on the user. If the base package's middleware sits in the web group, Nova's own routes (which use nova middleware) may not include it — check nova.middleware config and the package's route registration rather than assuming.
Unique validation must be context-scoped. unique:pages,slug will reject a legitimate slug that exists only in another site, or worse, allow a duplicate by validating against the wrong subset. Use Rule::unique(...)->where('site_id', context()->id) on every uniqueness rule on a contextable resource, and audit Slug/translatable fields for the same problem.
Escape hatches, used sparingly. withoutGlobalScope() in a resource, a lens, or an action is the correct tool for cross-context admin work — and the exact place a scope regression gets introduced. Grep for it during review, and prefer a named "all contexts" lens over scattered scope drops.
Debugging checklist. Context not applying? In order: (1) dump the resolved context inside indexQuery() — is it null, or the wrong one? (2) If null, the middleware is not running on the Nova route — check the route's middleware group. (3) If right on index but wrong in a relation field, the scope is not defined at model level. (4) If right everywhere but new records land in the wrong context, the create-path field default is missing. Log the context id in the service provider on boot in local env; it will save you an afternoon.
Maintenance posture. Zero stars and a single release in three years means: vendor-and-pin, expect to own it, and record the touched files in your upgrade runbook. The PHP surface is small enough to fork confidently — treat the Vue switcher and the scoping trait/scope as separable pieces so a future Laravel or Nova upgrade only forces you to rewrite the part that broke.
How can I help you explore Laravel packages today?