Global scopes allow isolating data by tenant automatically. Dedicated tenant models keep customer information securely separated.
Database schema design supports partitioning data into separate databases or tables for each customer. Global helpers seamlessly switch contexts between tenants.
Tenant-aware controllers check authentication and permissions scoped to individual customers. Isolated brandings, assets, and customizations give each their own unique experience from shared code.
Testing helpers seed demo data suited to multitenant apps. Migrations deployed per tenant establish independent data changes.
This post dives into practical implementations using Laravel’s multitenancy-ready features. From architectural pattern guides to concrete code examples, explore proven strategies that scale your solutions efficiently for many customers.
Database Schema for Supporting Multiple Tenants
There are two main approaches to structuring your database schema to support multiple tenants – using a separate database per tenant or partitioning data within a shared database.
For separate databases, each tenant gets their own isolated database instance. This provides ultimate separation but makes maintenance more complex.
For a shared database, you can either create a dedicated tenant table to store tenant information, or partition data at the table level by adding a tenant_id column to all relevant tables.
Partitioning at the table level is more efficient since it avoids the overhead of managing multiple database connections. To identify tenant-specific rows, all queries would be filtered by the tenant_id column.
// Add tenant_id to tables $table->unsignedBigInteger('tenant_id'); $table->foreign('tenant_id') ->references('id')->on('tenants') ->onDelete('cascade');
Tenant Table to Store Tenant Info
If using a shared database, you’ll need a central tenant table to store basic information for each customer.
// tenants table $table->id(); $table->string('name'); $table->string('domain')->unique(); $table->text('details')->nullable();
The tenant_id could then be used to reliably scope queries and segregate data per customer throughout your application.
Proper database structuring using these techniques ensures each tenant’s data remains isolated within your multitenant Laravel system.
The Tenant Model
The tenant model serves as the core entity representing each customer in the system. It stores essential metadata and establishes relationships to other tenant-specific data.
Tenant Model Fields
The minimum fields needed are:
// app/Models/Tenant. class Tenant extends Model { protected $primaryKey = 'id'; protected $fillable = [ 'name', 'domain', // other fields ]; }
Relations to Tenant Data
Relations associated with the tenant to custom content:
public function users() { return $this->hasMany(User::class); } public function pages() { return $this->hasMany(Page::class); }
Global Scopes for Tenant Filtering
Global scopes automatically filter queries by tenant:
public function scopeForTenant($query, $tenant) { return $query->where('tenant_id', $tenant->id); }
A base Tenant model with fields, relationships, and global scopes provides the foundation to isolate and access all tenant-specific information consistently throughout the application.
Tenant Aware Controllers
Controllers need awareness of the tenant context to filter responses appropriately.
Adding Tenant ID to Routes/Requests
A middleware can attach the tenant to the request, ie:
// Add tenant ID to requests $request->merge(['tenant' => $tenant]);
Tenant Checks in Controller Actions
Validate tenant before executing logic:
public function index(Request $request) { $this->authorizeTenant($request->tenant); // query for tenant return User::forTenant($request->tenant)->get(); }
Accessing Tenant Data
Retrieve tenant from request to scope queries:
protected function authorizeTenant($tenant) { $this->tenant = $tenant; } public function create(Request $request) { Page::create([ 'title' => $request->title, 'tenant_id' => $this->tenant->id ]); }
Actions can now rely on the attached tenant object to target correct multi-tenant data and responses for that customer.
Tenant Specific Data
Tenants will often need custom styling, configurations, or content unique to their needs. Migration namespaces help organize this data.
Namespaced Migrations
Migrations to create tenant fields live in a namespace:
artisan make:migration create_tenant_settings_table --path=Database/Tenants/Migrations
Approach for Shared vs Separate Data
Some data like user profiles can live together partitioned by tenant_id.
But tenant-specific configurations/files are best stored separately:
// Save settings to tenants' table $tenant->settings = ['key' => 'value']; $tenant->save(); // Save custom CSS to file for each tenant File::put("public/css/tenants/{$tenant->id}.css", $css);
This trains developers to think of tenant data location – centralized or isolated. Proper structuring maintains a balance between flexibility and isolation.
Namespaced migrations and selective separation of tenant information implement a scalable strategy for multi-tenant databases in Laravel apps.
Tenant Specific Content
Each tenant can have their own unique content customized for their business needs.
Tenant Settings/Configuration Options
Tenants may need to configure application behavior:
// Get tenant settings $settings = $tenant->settings; // Common settings stored in tenants table $fillable = ['name', 'timezone', 'stylesheet'];
Tenant Branding/Customization
Allow custom logos, colors etc:
// Store logo image path $tenant->logo = '/uploads/'.$logo->hashName(); // Save custom stylesheet File::put("CSS/{$tenant->id}.css", $customCSS);
Isolated Assets/Files for Each Tenant
Files like images & documents stay separated to avoid conflicts:
// Unique folder for each tenant $assetPath = "files/{$tenant->id}/"; // Save & retrieve tenant file Storage::put($assetPath.$fileName, $file); $file = Storage::get($assetPath.$fileName);
Giving tenants independence over these areas within a shared application enhances their experience through a customized interface and content relevant only to their business needs and brand.
Authentication
Authentication methods must account for the tenant context to properly assign users.
Tenant-Aware Authentication Methods
During login, determine the user’s tenant:
// Get tenant on login $tenant = Tenant::findByDomain(request('domain')); // Authenticate for that tenant Auth::guard('users')->attempt(['email','password'], $tenant);
Assigning Users to Tenants
Connect users to their tenants:
// User model public function tenant(){ return $this->belongsTo(Tenant::class); } // Assign on user create $user->tenant()->associate($tenant); $user->save();
User Permissions Scoping by Tenant
Authorize actions within a tenant:
// Check permission $this->authorize('update-profile', $user->tenant); // Policy method public function updateProfile($tenant){ return $this->user->tenant_id == $tenant->id; }
Proper user-tenant relationships and scoping permissions by tenant integrate authorization into the multitenant architecture.
Tenant Switching
Allow authenticated users to switch between tenants they have access to.
Tenant User Portal for Switching
Present a user portal to select tenants:
// User dashboard @foreach($user->accessibleTenants as $tenant) <a href="{{ route('switch-tenant', $tenant) }}"> {{ $tenant->name }} </a> @endforeach
Global Tenant Helper/Middleware
Attach new tenant to request on a switch:
// Middleware public function handle($request, Closure $next) { $tenant = Tenant::find($request->route('tenant_id')); session()->put('tenant_id', $tenant->id); return $next($request); } // Helper function currentTenant() { return Tenant::find(session('tenant_id')); }
This maintains the correct tenant context across requests as the user navigates, allowing them to manage multiple customer environments through a single portal.
Testing Multitenant Apps
Proper test data setup is crucial for multi-tenant apps to evaluate each tenant in isolation.
Tenant Seeding for Testing
Seed a test tenant before each test:
// DatabaseSeeder $tenant = Tenant::factory()->create(); $this->call(TenantDataSeeder::class, ['tenant' => $tenant]);
Isolating Test Data per Tenant
Seed tenant-specific data like users:
// TenantDataSeeder public function run(Tenant $tenant) { User::factory()->count(5)->create([ 'tenant_id' => $tenant->id ]); }
Tests can then make assertions against isolated tenant data:
// User test $tenant = Tenant::first(); $users = User::forTenant($tenant)->get(); $this->assertCount(5, $users);
Maintaining test isolation prevents data collisions between tenants. Helper methods also allow dynamic seeding per SUT.
Comprehensive testing ensures that multi-tenant applications function correctly for each unique tenant configuration, customization, and data set.
Conclusion
Laravel provides powerful features that allow developers to easily build robust multi-tenant applications. The framework’s tooling for scoping queries, isolating data, and tenant-aware patterns supports seamlessly managing multiple customers from a single codebase.
By leveraging techniques like dedicated tenant models, global scopes, migration namespaces, and request context binding, both data and user experiences can remain securely separated while sharing code. Developers gain a straightforward and consistent approach for filtering responses according to the tenant.
Proper database structuring through tenant tables or partitioning further strengthens each customer’s private domain. Combined with rating scoping to tenants, authentication falls naturally into the architecture.
Comprehensive tenant models also promote strong relationships and organization of tenant-specific information. Ultimately this leads to focused code, clear tenant identities, and flexible customizations unique to individual business needs.
Laravel’s built-in tenant utilities encourage disciplined multi-tenant designs capable of scaling complexity while retaining simplicity. With best practices for tenant switching, testing isolation, and content management applied, Endless Growth handles evolving business volumes seamlessly.