Tenant Schema Resetting Too Early: A Laravel Livewire Gotcha
The Reimpact/platform project focuses on providing a multi-tenant application structure. Recently, we encountered an issue with tenant database schemas being reset prematurely during Livewire updates. This post dives into the problem and the solution implemented.
The Problem
In a multi-tenant application, each tenant typically has its own database schema. We use middleware to set the correct schema for each request. However, with Livewire's persistent middleware, we found that the schema was being reset before the Livewire component had a chance to execute its queries. This resulted in "relation does not exist" errors, as the component was trying to access tables in the wrong schema.
The original middleware used a try/finally block to ensure the schema was always reset. However, Livewire's middleware execution order meant that the finally block was running too early.
The Solution
Instead of using finally, we switched to using the terminate() method in the middleware. This method is called after the response has been sent to the browser, ensuring that the schema is reset only after all component queries have executed. Additionally, we added a fallback mechanism to resolve the tenant based on the current user if Filament::getTenant() returns null during Livewire updates. This handles cases where the tenant context might not be immediately available.
Here's a simplified example of the middleware:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SetTenantSchema
{
public function handle(Request $request, Closure $next)
{
$tenantId = $request->header('X-Tenant-ID'); // Example: Get tenant ID from header
if ($tenantId) {
DB::statement('SET search_path = tenant_' . $tenantId);
}
return $next($request);
}
public function terminate(Request $request, $response)
{
DB::statement('SET search_path = public');
}
}
In the handle method, we dynamically set the PostgreSQL search_path based on a tenant identifier (in this example, taken from a request header). The terminate method then resets the search_path back to the default public schema after the request is complete.
The Takeaway
When working with middleware in Laravel, especially with technologies like Livewire, be mindful of the execution order and lifecycle. Using terminate() instead of finally can be crucial when dealing with resources that need to persist for the duration of the request but must be cleaned up afterward. Always consider the specific context and timing of your middleware actions to avoid unexpected behavior in long-running processes or during asynchronous updates.