Overview
Multi-tenancy allows multiple organizations to use the platform while keeping their data completely isolated. Each tenant operates independently with their own products, categories, orders, and team members.
How It Works
Tenant Resolution
The system identifies the current tenant from the X-Tenant-ID header:
This header should be included in all API requests that require tenant context.
Automatic Scoping
Models using the HasTenant trait are automatically filtered:
Laravel Model
Query Examples
use App\ HasTenant ;
class Product extends Model
{
use HasTenant ;
protected $fillable = [ 'name' , 'price' ];
// tenant_id automatically managed
}
Tenant-Scoped Models
These models belong to a single tenant:
Catalog
Products
Categories
Product Images
Operations
Orders
Order Items
Ratings
Integrations
Webhooks
Integration Outbox
Cross-Tenant Models
These models exist across tenants:
User - Can belong to multiple tenants
Tenant - The organization itself
TenantUser - Pivot table linking users to tenants
User Roles
Each user has a role per tenant:
Full Control
Add/remove team members
Update member roles
Manage products and categories
Configure webhooks
Update tenant settings
Best Practices
1. Always Include X-Tenant-ID
curl -X GET https://api.example.com/api/v2/admin/products \
-H 'Authorization: Bearer TOKEN' \
-H 'X-Tenant-ID: 123'
2. Verify Tenant Membership
Before accessing tenant resources:
$isMember = auth () -> user () -> tenants ()
-> where ( 'tenant_id' , $tenantId )
-> exists ();
if ( ! $isMember ) {
abort ( 403 , 'Access denied' );
}
3. Check Role for Sensitive Operations
$isAdmin = auth () -> user () -> tenants ()
-> where ( 'tenant_id' , $tenantId )
-> wherePivot ( 'role' , 'ADMIN' )
-> exists ();
if ( ! $isAdmin ) {
abort ( 403 , 'Admin access required' );
}
4. Don’t Manually Set tenant_id
The HasTenant trait handles this automatically:
// ❌ Bad - manual assignment
$product = new Product ();
$product -> tenant_id = $tenantId ;
$product -> save ();
// ✅ Good - automatic from X-Tenant-ID
$product = Product :: create ([
'name' => 'Product Name' ,
'price' => 99.99
]);
Security Considerations
Always verify tenant membership before showing data. The system returns empty results for unauthorized tenants rather than throwing errors, maintaining a consistent API experience.
Tenant Isolation
Data is automatically scoped by tenant
Cross-tenant queries are prevented
Users can only access their tenants’ data
Role Verification
ADMIN role required for team management
Role checks happen at the controller level
Middleware validates tenant access
Testing Multi-Tenancy
public function test_user_cannot_see_other_tenant_data ()
{
$tenant1 = Tenant :: create ([ 'name' => 'Tenant 1' ]);
$tenant2 = Tenant :: create ([ 'name' => 'Tenant 2' ]);
$product1 = Product :: forTenant ( $tenant1 -> id )
-> create ([ 'name' => 'Product 1' ]);
$product2 = Product :: forTenant ( $tenant2 -> id )
-> create ([ 'name' => 'Product 2' ]);
// Set tenant context
$this -> withHeader ( 'X-Tenant-ID' , $tenant1 -> id );
// Should only see tenant1's products
$products = Product :: all ();
$this -> assertCount ( 1 , $products );
$this -> assertEquals ( 'Product 1' , $products -> first () -> name );
}
Common Patterns
Switching Tenants
Users can switch between their tenants by changing the X-Tenant-ID header:
// Get user's tenants
const { tenants } = await api . get ( '/api/v2/admin/who_am_i' );
// Switch to different tenant
api . defaults . headers [ 'X-Tenant-ID' ] = tenants [ 0 ]. id ;
Creating Tenant-Scoped Resources
// Create product in current tenant
await axios . post ( '/api/v2/admin/products' , {
name: 'New Product' ,
price: 29.99
}, {
headers: {
'Authorization' : `Bearer ${ token } ` ,
'X-Tenant-ID' : currentTenantId
}
});