Multi-Tenancy
The system is multi-tenant: one Laravel application serves multiple law firms, each with
complete data isolation. The tenant boundary is the law_firms table. Every piece of
firm data carries a law_firm_id foreign key.
How Filament Tenancy Works
Filament v3 has built-in tenant support. Registering LawFirm as the tenant model causes
Filament to:
- Prefix all panel URLs with the tenant slug:
/admin/{firm-slug}/matters - Inject the current tenant into every Eloquent query via a global scope
- Redirect unauthenticated or cross-tenant requests automatically
// app/Providers/Filament/AdminPanelProvider.php
->tenant(LawFirm::class, slugAttribute: 'slug')
->tenantMiddleware([ApplyTenantScopes::class], isPersistent: true)
The LawFirm Model
// app/Models/Firm/LawFirm.php
class LawFirm extends Model implements HasTenants
{
public function getTenantSlug(): string
{
return $this->slug;
}
public function canAccessTenant(Model $tenant): bool
{
// User must have an active FirmUserProfile for this firm
return $this->firmUserProfiles()
->where('user_id', auth()->id())
->where('is_active', true)
->exists();
}
}
Scoping Models to the Tenant
Every model that belongs to a firm implements a global scope that filters by the current
tenant automatically. This means every Matter::all() call returns only matters for the
current firm — no developer needs to remember to add ->where('law_firm_id', ...).
// app/Models/Matter/Matter.php
class Matter extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new TenantScope);
}
}
// app/Models/Scopes/TenantScope.php
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
if (Filament::hasTenant()) {
$builder->where('law_firm_id', Filament::getTenant()->id);
}
}
}
Tables that require tenant scoping (carry law_firm_id):
law_firms ← the tenant itself
firm_user_profiles matters clients
client_users partner_firms firm_partner_relationships
user_role_assignments resource_access_grants
billing_activity_codes firm_tax_rates lawyer_rate_plans
invoices documents appointments
support_access_sessions admin_resource_locks
Three User Types and Tenant Resolution
Each panel resolves its tenant differently:
| Panel | Guard | Tenant resolved from |
|---|---|---|
Admin /admin | web | URL slug → law_firms.slug |
Client /portal | client | client_users.user_id → client_contacts.client_id → clients.law_firm_id |
Partner /partner | partner | partner_attorney_users.user_id → partner_attorneys.partner_firm_id → firm_partner_relationships.law_firm_id |
Octane Safety for Tenant Context
Under Octane, the tenant must be resolved fresh on every request — it must never be stored
in a singleton. Use scoped() bindings for any class that holds the current tenant:
// AppServiceProvider.php
$this->app->scoped(CurrentTenant::class, fn() => new CurrentTenant());
// NEVER:
$this->app->singleton(CurrentTenant::class, fn() => new CurrentTenant());
// ↑ This leaks firm A's tenant context into firm B's next request