Claude-skill-registry laravel-multi-tenancy
Multi-tenant application architecture patterns. Use when working with multi-tenant systems, tenant isolation, or when user mentions multi-tenancy, tenants, tenant scoping, tenant isolation, multi-tenant.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/laravel-multi-tenancy" ~/.claude/skills/majiayu000-claude-skill-registry-laravel-multi-tenancy && rm -rf "$T"
manifest:
skills/data/laravel-multi-tenancy/SKILL.mdsource content
Laravel Multi-Tenancy
Multi-tenancy separates application logic into central (non-tenant) and tenanted (tenant-specific) contexts.
Related guides:
- tenancy-testing.md - Testing multi-tenant features
- Actions - Central vs Tenanted action organization
- Models - Model organization
- structure.md - Directory organization
Philosophy
Multi-tenancy provides:
- Clear separation between central and tenant contexts
- Database isolation with separate databases per tenant
- Automatic scoping of queries to current tenant
- Context awareness through helper classes
- Queue integration with tenant context preservation
When to Use
Use multi-tenancy when:
- Building SaaS applications with complete data isolation
- Each customer needs their own database
- Compliance requires strict data separation
Don't use when:
- Simple user segmentation is sufficient (use user_id scoping)
- All customers share the same schema
- Application complexity doesn't justify the overhead
Directory Structure
app/ ├── Actions/ │ ├── Central/ # Non-tenant actions │ │ ├── Tenant/ │ │ │ ├── CreateTenantAction.php │ │ │ └── DeleteTenantAction.php │ │ └── User/ │ │ └── CreateCentralUserAction.php │ └── Tenanted/ # Tenant-specific actions │ ├── Order/ │ │ └── CreateOrderAction.php │ └── Customer/ │ └── CreateCustomerAction.php ├── Data/ │ ├── Central/ # Central DTOs │ └── Tenanted/ # Tenant DTOs ├── Http/ │ ├── Central/ # Central routes (tenant management) │ ├── Web/ # Tenant application routes │ └── Api/ # Public API (tenant-scoped) ├── Models/ # All models in standard location │ ├── Tenant.php # Central model │ ├── Order.php # Tenanted model │ └── Customer.php └── Support/ └── TenantContext.php
Central Actions
Central actions manage tenants and cross-tenant operations.
<?php declare(strict_types=1); namespace App\Actions\Central\Tenant; use App\Data\Central\CreateTenantData; use App\Models\Tenant; use Illuminate\Support\Facades\DB; class CreateTenantAction { public function __construct( private readonly CreateTenantDatabaseAction $createDatabase, ) {} public function __invoke(CreateTenantData $data): Tenant { return DB::transaction(function () use ($data): Tenant { $this->guard($data); $tenant = $this->createTenant($data); ($this->createDatabase)($tenant); return $tenant; }); } private function guard(CreateTenantData $data): void { throw_if( Tenant::where('domain', $data->domain)->exists(), TenantDomainAlreadyExistsException::forDomain($data->domain) ); } private function createTenant(CreateTenantData $data): Tenant { return Tenant::create([ 'id' => $data->tenantId, 'name' => $data->name, 'domain' => $data->domain, ]); } }
Tenanted Actions
Tenanted actions operate within a specific tenant's context. All queries automatically scoped.
<?php declare(strict_types=1); namespace App\Actions\Tenanted\Order; use App\Data\Tenanted\CreateOrderData; use App\Models\Order; use App\Models\User; use Illuminate\Support\Facades\DB; class CreateOrderAction { public function __invoke(User $user, CreateOrderData $data): Order { return DB::transaction(function () use ($user, $data): Order { // Automatically scoped to current tenant $order = $user->orders()->create([ 'status' => $data->status, 'total' => $data->total, ]); $this->createOrderItems($order, $data->items); return $order; }); } private function createOrderItems(Order $order, array $items): void { foreach ($items as $item) { $order->items()->create([ 'product_id' => $item->productId, 'quantity' => $item->quantity, 'price' => $item->price, ]); } } }
Tenant Context Helper
<?php declare(strict_types=1); namespace App\Support; use App\Models\Tenant; use Stancl\Tenancy\Facades\Tenancy; class TenantContext { public static function current(): ?Tenant { return Tenancy::tenant(); } public static function id(): ?string { return Tenancy::tenant()?->getTenantKey(); } public static function isActive(): bool { return Tenancy::tenant() !== null; } public static function run(Tenant $tenant, callable $callback): mixed { return tenancy()->runForMultiple([$tenant], $callback); } public static function runCentral(callable $callback): mixed { return tenancy()->runForMultiple([], $callback); } }
Usage:
use App\Support\TenantContext; $tenant = TenantContext::current(); $tenantId = TenantContext::id(); if (TenantContext::isActive()) { // Tenant-specific logic } TenantContext::run($tenant, function () { Order::create([...]); }); TenantContext::runCentral(function () { Tenant::create([...]); });
Tenant Identification Middleware
Domain-Based
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; class IdentifyTenant extends InitializeTenancyByDomain { // Tenant identified by domain (e.g., tenant1.myapp.com) }
Subdomain-Based
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; class IdentifyTenant extends InitializeTenancyBySubdomain { // Tenant identified by subdomain }
Header-Based
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; class IdentifyTenant extends InitializeTenancyByRequestData { public static string $header = 'X-Tenant'; }
Route Configuration
Tenant Routes
// routes/tenant.php Route::middleware(['tenant'])->group(function () { Route::get('/orders', [OrderController::class, 'index']); Route::post('/orders', [OrderController::class, 'store']); });
Central Routes
// routes/central.php Route::middleware(['central'])->prefix('central')->group(function () { Route::get('/tenants', [TenantController::class, 'index']); Route::post('/tenants', [TenantController::class, 'store']); });
Bootstrap Configuration
return Application::configure(basePath: dirname(__DIR__)) ->withRouting(function () { Route::middleware('web') ->prefix('central') ->name('central.') ->group(base_path('routes/central.php')); Route::middleware(['web', 'tenant']) ->group(base_path('routes/tenant.php')); }) ->create();
Models
All models live in
app/Models/. Central vs tenanted distinguished by traits/interfaces, not subdirectories.
Central Model
<?php declare(strict_types=1); namespace App\Models; use Stancl\Tenancy\Database\Models\Tenant as BaseTenant; class Tenant extends BaseTenant { public function users(): HasMany { return $this->hasMany(User::class); } }
Tenanted Model
<?php declare(strict_types=1); namespace App\Models; use Illuminate\Database\Eloquent\Model; class Order extends Model { // Automatically scoped to current tenant // No tenant_id needed in queries public function user(): BelongsTo { return $this->belongsTo(User::class); } }
Queue Jobs with Tenant Context
Jobs must preserve tenant context when queued.
<?php declare(strict_types=1); namespace App\Jobs\Tenanted; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Jobs\TenantAwareJob; class ProcessOrderJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, TenantAwareJob; public function __construct( public TenantWithDatabase $tenant, public OrderData $orderData, ) { $this->onQueue('orders'); } public function handle(ProcessOrderAction $action): void { // Runs in tenant context automatically $action($this->orderData); } }
Dispatching:
ProcessOrderJob::dispatch(TenantContext::current(), $orderData);
Common Patterns
Running Code in Multiple Tenants
$tenants = Tenant::all(); foreach ($tenants as $tenant) { TenantContext::run($tenant, function () use ($tenant) { Order::where('status', 'pending')->update(['processed' => true]); }); }
Accessing Central Data from Tenant Context
TenantContext::runCentral(function () { $allTenants = Tenant::all(); });
Conditional Logic Based on Tenant
if (TenantContext::isActive()) { $orders = Order::all(); // Scoped to tenant } else { $tenants = Tenant::all(); // Central }
Testing
→ Complete testing guide: tenancy-testing.md
Includes:
- Testing central and tenanted actions
- ManagesTenants and RefreshDatabaseWithTenant traits
- TenantTestCase setup
- Pest configuration for multi-tenancy
- Test directory structure
Summary
Multi-tenancy provides:
- Clear separation - Central vs Tenanted namespaces
- Database isolation - Each tenant has dedicated database
- Automatic scoping - Queries automatically tenant-scoped
- Context helpers - Easy access to tenant context
- Queue integration - Jobs preserve tenant context
Best practices:
- Use directory structure to separate central and tenanted actions/DTOs
- Keep models in
following Laravel conventionapp/Models/ - Always use TenantContext helper for tenant access
- Test both central and tenant contexts separately
- Preserve tenant context in queued jobs