ARTICLE AD BOX
Laravel 11.35.1 | PHP 8.4.2 | PostgreSQL 16.5
I'm implementing a multi-tenant SaaS with polymorphic relations across 3 database connections (tenant1, tenant2, shared). The issue occurs only in queue jobs where pivot constraints fail due to tenant context loss.
Database Schema
SQL
-- shared database tenants id (uuid), name, database_name -- tenant1 database users id (uuid), tenant_id (uuid), name, email workspaces id (uuid), tenant_id (uuid), name -- tenantX databases (dynamic) workspace_members (pivot) id (uuid), workspace_id (uuid), member_id (uuid), member_type -- member_type: 'App\Models\User', 'App\Models\Team', 'App\Models\ExternalGuest'Models
PHP
// app/Models/Tenant.php (shared DB) class Tenant extends Model { protected $connection = 'shared'; public function database() { return $this->belongsToMany(Workspace::class, 'workspace_members', 'tenant_id', 'workspace_id')->withPivot('member_id', 'member_type'); } } // app/Models/Workspace.php (tenant DB) class Workspace extends Model { protected $connection = null; // Dynamic public function members() { return $this->morphToMany( get_class(), // Dynamic: User|Team|ExternalGuest 'member', 'workspace_members', 'workspace_id', 'member_id' )->withPivot('tenant_id'); // Tenant isolation } public function membersInCurrentTenant() { return $this->members()->wherePivot('tenant_id', tenant()->id); } } // Dynamic model resolution class DynamicModelResolver { public static function resolve(string $type): Model { $tenantId = tenant()->id; // Switch DB connection config(['database.connections.tenant.database' => "tenant_{$tenantId}"]); DB::purge('tenant'); DB::reconnect('tenant'); return match($type) { 'App\Models\User' => User::on('tenant'), 'App\Models\Team' => Team::on('tenant'), 'App\Models\ExternalGuest' => ExternalGuest::on('tenant') }; } }The Problem
Works in HTTP requests ✅
PHP
// In controller - WORKS $workspace = Workspace::find('uuid'); $members = $workspace->membersInCurrentTenant()->get(); // Returns only members where pivot.tenant_id = current_tenantFails in Queue Jobs ❌
PHP
// In queue job - BROKEN class ProcessWorkspaceActivity extends Job { public function handle() { $workspace = Workspace::find('uuid'); $members = $workspace->membersInCurrentTenant()->get(); // Returns ALL members across ALL tenants! // pivot.tenant_id constraint is ignored } }Debug Output
HTTP Request:
text
SQL: select * from workspace_members where workspace_id = ? and tenant_id = 'tenant-uuid-here'Queue Job:
text
SQL: select * from workspace_members where workspace_id = ? -- NO tenant_id constraint!What I've Tried
Tenant Middleware in Jobs ❌PHP
class TenantAwareJob extends Job { public $tenantId; public function handle() { tenant($this->tenantId); // Sets global tenant // Still broken } } Global Scopes ❌PHP
// WorkspaceMemberObserver class WorkspaceMemberObserver { public function retrieved(WorkspaceMember $model) { if (!tenant()) return; $model->where('tenant_id', tenant()->id); } } Connection Switching ❌PHP
DB::connection('tenant')->table('workspace_members') ->where('workspace_id', $id) ->where('tenant_id', tenant()->id); // Works but bypasses EloquentMinimal Reproducible Example
PHP
// DatabaseSeeder DB::connection('tenant1')->table('workspace_members')->insert([ ['workspace_id' => 'ws1', 'member_id' => 'user1', 'member_type' => 'App\Models\User', 'tenant_id' => 'tenant1'], ['workspace_id' => 'ws1', 'member_id' => 'user2', 'member_type' => 'App\Models\User', 'tenant_id' => 'tenant2'], // Cross-tenant! ]); // Test job php artisan queue:work --onceExpected: 1 member (tenant1 only) Actual: 2 members (both tenants)
Questions
Why does the pivot constraint disappear in queue jobs?
How to preserve tenant context in Eloquent relations during queue processing?
Best practice for multi-tenant polymorphic relations with dynamic DB connections?
Current Workaround (ugly)
PHP
// Forces tenant constraint manually $members = $workspace->members() ->whereHas('pivot', fn($q) => $q->where('tenant_id', tenant()->id)) ->get();But this breaks:
with('members') eager loading
members()->count()
Any relation chaining
Has anyone solved multi-tenant polymorphic relations in Laravel queues? 🤯
