Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Simple Tenancy Laravel Package

sbine/simple-tenancy

Simple multi-tenancy for Laravel: add a trait to scope Eloquent models to the current tenant, with automatic query filtering and tenant-aware model creation. Lightweight and straightforward, it's ideal for apps that need clean tenant isolation without per-tenant databases.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install with composer require sbine/simple-tenancy. It is MIT-licensed and lightweight, so expect a small service provider rather than a full tenancy stack (no database-per-tenant orchestration, no tenant-aware queue/cache drivers out of the box).
  • Publish the config first: php artisan vendor:publish --provider="Sbine\SimpleTenancy\SimpleTenancyServiceProvider" (confirm the exact provider FQN in the README — packages of this size often expose a config tag like --tag=tenancy).
  • Read three files before writing any code, in this order: the README, config/tenancy.php (if published), and the package's middleware + trait. The middleware is where tenant resolution happens, and the trait is where query scoping happens. Those two files define the entire mental model of the package.
  • Register the middleware in your tenancy-aware route group (usually web middleware stack or a named tenant group) — do not drop it in global until you've verified it is idempotent.
  • Add a nullable, indexed tenant_id column to every model you want scoped, then apply the package's tenant trait to those models. The trait typically bootstraps a global scope that appends where tenant_id = <current> automatically.
  • First use case: a tenants table with id, slug/domain, and a posts table with tenant_id. Resolve the tenant by subdomain in middleware, apply the trait to Post, and hit /acme/posts. If the query returns only Acme's rows without you writing a where, the package is wired correctly.

Implementation Patterns

  • Resolution lives in middleware. Keep tenant resolution (subdomain, X-Tenant header, session value, or route model binding) in one middleware class. Everything downstream — global scopes, the tenant helper, relationship defaults — depends on that context being set before the request hits a controller.
  • Trait-per-model, not per-project. Apply the tenant trait only to models that actually belong to a tenant. Shared/global tables (users if global, plans, countries) should stay untraited, otherwise you'll spend time fighting scopes that shouldn't be there.
  • Auto-fill on create. Good simple-tenancy implementations fill tenant_id on model creation via a creating hook. If yours doesn't, add it in your model's booted() or a BelongsToTenant::creating listener — otherwise every ->create() call becomes a manual tenant assignment and someone will forget.
  • Explicit cross-tenant reads. For admin/back-office controllers that legitimately need all tenants, use Model::withoutGlobalScope(...) (or the package's equivalent opt-out) explicitly and locally. Do not disable the scope globally just to make one report work.
  • Route structure. Prefer a route group like:
    Route::middleware(['web', 'tenant'])->prefix('{tenant}')->group(function () {
        // tenant-scoped routes
    });
    
    and let the middleware resolve {tenant} into the package's current-tenant store. This keeps controllers free of resolution logic.
  • Relationships stay scoped automatically. Because the global scope applies at the query-builder level, $tenant->posts and Post::find() both honor the current tenant. That is the biggest day-to-day win — you stop passing tenant_id around in controllers.
  • Testing. In feature tests, resolve a tenant explicitly (hit the middleware with a subdomain, or call the package's setter if it exposes one) in setUp(). For unit tests of scoped models, wrap in the same helper so the global scope has something to bind to.
  • Seeder strategy. Seed per-tenant in a loop, resolving/switching tenant between iterations, rather than writing one big seeder with hardcoded tenant_id values. That mirrors production behavior and catches scope bugs.

Gotchas and Tips

  • Queued jobs lose the tenant context. The middleware only runs for HTTP requests. Any dispatch() that later touches a scoped model will run with no tenant bound — either serialize tenant_id into the job and re-bind in handle(), or the query will fail/leak. Add a TenantAware job trait early.
  • Console/artisan has no tenant. php artisan migrate, seeders, and any custom commands run without a resolved tenant. Global scopes on artisan-invoked queries will silently return nothing (or throw). Provide a --tenant= flag or an explicit resolver call in command signatures.
  • withoutGlobalScopes() is a footgun. Calling it to "fix" a suspiciously empty result usually hides a mis-resolved tenant rather than the real bug. Log the resolved tenant id in middleware first.
  • Config caching. If you edit config/tenancy.php, run php artisan config:clear. On this package size, the config is often read once at boot — a stale cached config gives confusing "my change did nothing" symptoms.
  • Migration ordering. Add tenant_id (and its index, ideally a composite (tenant_id, created_at) for list screens) before you attach the trait and run queries, or the scope will reference a column that doesn't exist yet.
  • Uniqueness must be per-tenant. Validate email/slug uniqueness with a rule scoped to the tenant (unique:users,email,NULL,id,tenant_id,{$tenant->id}). Global unique constraints are a classic tenancy bug: tenant B can never register an email tenant A already used.
  • Middleware order matters. Tenant middleware must run after session/auth middleware (if you resolve from session) and before anything that queries tenant-scoped models — including route model binding, which resolves before the controller.
  • Extension point. The package is small, so rather than forking, wrap it: subclass the middleware or bind your own current-tenant resolver in a service provider. Keep tenant resolution behind an interface (CurrentTenant) so you can swap subdomain → header → token without touching the scope.
  • Cache keys. The package doesn't namespace your cache for you. Prefix cache keys with the tenant id (cache()->remember("tenant:{$id}:stats", ...)) or you'll serve Acme's dashboard numbers to Globex.
  • Score/star reality check. At ~37 stars this package is unmaintained-adjacent. Pin the version in composer.json, read the scope and middleware code yourself (they're small), and have a plan to inline those ~100 lines if you outgrow it. For anything beyond simple single-DB row scoping, evaluate stancl/tenancy instead.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource