genealabs/nova-multi-tenant-manager
GeneaLabs Nova Multi-Tenant Manager adds multi-tenancy support to Laravel Nova: manage tenants, scope resources and queries per tenant, and let users switch tenants so your Nova admin panel stays tenant-aware.
Do not composer require this. The repo is archived (last ship 2024‑05‑11), pre‑1.0, ~20 stars, zero dependents. Day‑to‑day leverage comes from treating it as a spec + starting source, not a dependency. Budget a half‑day to a day to evaluate, 1–2 weeks to own a safe fork.
Prerequisites before you spend any time:
laravel-multitenancy, single‑DB with scoping, or per‑tenant DBs). This package is an admin UI over tenancy, not tenancy itself. If tenancy isn't built, stop; you'd be building a UI over unbuilt work.Minimal bootstrap (fork path):
# 1. Mirror, don't vendor-point at upstream
git clone https://github.com/mikebronner/nova-multi-tenant-manager ../internal/nova-tenant-manager
rm -rf ../internal/nova-tenant-manager/.git # re-init under your namespace
# 2. Consume via path repo while spiking
composer config repositories.nova-tenant-manager path ../internal/nova-tenant-manager
composer require genealabs/nova-multi-tenant-manager:"*@dev"
Record upstream URL, archival date, commit SHA, license, and a named owner in your dependency register before the first commit. That artifact is what auditors and your successor will ask for.
Where to look first, in this order:
NovaServiceProvider registration — that's the surface you actually get.Tenant and Domain Nova resources — this is the CRUD you'd otherwise write yourself.makeCurrent() (or equivalent) on the tenancy package — this is the security-critical file, not the UI.config/ — publish it into your app and commit it, so behaviour versions with your app rather than with vendor code.domains/tenant pivots, or assumes yours already exist.First use case to validate: a support engineer, behind SSO and a role check, switches current tenant, opens a tenant‑scoped Nova resource, and sees only that tenant's rows — and a second engineer's switch does not bleed across. If that one flow works and you can prove the negative case, the rest is CRUD.
Register the tool explicitly. Nova tools are opt‑in per panel; don't assume auto‑discovery surfaces it.
// app/Providers/NovaServiceProvider.php
public function tools(): array
{
return [
new \App\Nova\Tools\TenantManager, // your fork's class, your namespace
];
}
Wrap it behind one adapter. You want exactly one file to touch when Nova ships a major.
// app/Support/Tenancy/TenantSwitcher.php
final class TenantSwitcher
{
public function switchTo(Tenant $tenant, User $actor): void
{
abort_unless($actor->can('switchTenant', $tenant), 403);
activity('tenancy')->causedBy($actor)
->withProperties(['from' => Tenant::current()?->id, 'to' => $tenant->id])
->log('tenant switched');
$tenant->makeCurrent();
}
public function forget(): void { Tenant::forgetCurrent(); }
}
Every call site goes through this — controllers, actions, tests. When the fork rots (it will), you replace one class.
Scope tenant‑aware resources by opt‑in trait, then prove it. The classic failure is a new Nova resource that forgets the trait and fails open — it returns all tenants' rows. Institutionalise both halves:
// App\Nova\Concerns\BelongsToCurrentTenant (applied per resource)
// plus a Pest test per resource:
it('does not leak across tenants', function () {
$a = Tenant::factory()->create(); $b = Tenant::factory()->create();
$aPost = Post::factory()->for($a)->create();
actingAsNovaAdmin()->getJson("/nova-api/posts/{$aPost->id}")
->assertNotFound(); // after switching to $b
});
Provisioning actions must be queued jobs, never inline. Tenant creation, domain attach, per‑tenant migrations — dispatch with an explicit tenant id in the payload and re‑establish context in the job's handle(). Relying on ambient "current tenant" in a queued action is the #1 way this leaks.
Domain management: normalise on write. Lowercase, strip scheme/www, unique index on domains.domain. Do it in a saving observer on your model, not in the Nova field, so seeders and imports get the same treatment.
User↔tenant assignment lives in policies, not the UI. The pivot UI is trivial; deciding who may grant access is not. One policy (TenantMembershipPolicy) with attachAnyUser / detachAnyUser gates, and canSee/canRun on the Nova action mirroring it.
Keep an operator canary. Ship to internal support/ops first, behind SSO. A bug there is a confused agent; the same bug on a customer-facing switcher is a cross‑tenant incident.
^0.7 at minimum, a commit SHA preferably. Never dev-main, never *. An archived repo can still be force‑pushed or deleted; your mirror kills that failure mode.Authenticate and authorization middleware, or you get an unauthenticated tenant switch. Test it with a guest request, not just as an admin.makeCurrent() in a long‑running worker (Octane, queue) persists between jobs. Terminate with forgetCurrent() in a terminable middleware and at the top of each queued action's handle(). This package predates any Octane guarantee — assume it's missing.use BelongsToCurrentTenant, and a per‑resource regression test. Review‑checklist item, every PR.config:cache after publishing; the published config is what your CI matrix should test against, not the package default. Test with --env=production-ish cached config at least once.composer why-not is your only debugging tool here. With zero dependents there are no known‑good combinations. When a conflict appears, it appears at upgrade time, when your options are fewest — resolve illuminate/* constraints early.How can I help you explore Laravel packages today?