Understanding ShelfWise’s multi-tenant architecture and tenant isolation
ShelfWise is built as a multi-tenant application from the ground up. Each business operates as an isolated tenant with complete data separation and independent configuration.
Multi-tenancy allows multiple organizations (tenants) to use the same ShelfWise installation while keeping their data completely separate. Each tenant has:
Isolated database records
Independent user accounts
Separate shops and inventory
Custom configuration and settings
Isolated financial data
Think of each tenant as a completely separate business using ShelfWise. Tenant A cannot see or access any data from Tenant B.
Models use the BelongsToTenant trait for automatic tenant scoping:
app/Traits/BelongsToTenant.php
trait BelongsToTenant{ protected static function bootBelongsToTenant() { static::addGlobalScope('tenant', function ($query) { if (auth()->check() && auth()->user()->tenant_id) { $query->where('tenant_id', auth()->user()->tenant_id); } }); } public function tenant() { return $this->belongsTo(Tenant::class); }}
Example usage:
app/Models/Product.php
class Product extends Model{ use BelongsToTenant; // Queries are automatically scoped to current user's tenant}
CRITICAL: Every database query MUST filter by tenant_id. This is enforced by the BelongsToTenant trait and global scopes, but always verify when writing raw queries.
// Automatic tenant scoping via global scope$products = Product::query()->where('is_active', true)->get();// Or explicitly filter by tenant$products = Product::query() ->where('tenant_id', auth()->user()->tenant_id) ->get();// Using services (recommended)$products = app(ProductService::class)->getAllProducts();
Tenants have resource limits based on their subscription:
// Check if tenant can create more shopsif ($tenant->hasReachedShopLimit()) { return back()->withErrors([ 'error' => 'You have reached your shop limit. Please upgrade your plan.' ]);}// Check user limitif ($tenant->hasReachedUserLimit()) { return back()->withErrors([ 'error' => 'You have reached your user limit. Please upgrade your plan.' ]);}// Check product limitif ($tenant->hasReachedProductLimit()) { return back()->withErrors([ 'error' => 'You have reached your product limit. Please upgrade your plan.' ]);}
Super admins can switch between tenants for support purposes:
// Check if user is super adminif ($user->isSuperAdmin()) { // Allow tenant switching session(['impersonate_tenant_id' => $tenantId]);}// In global scope, check for impersonationif (auth()->user()->isSuperAdmin() && session('impersonate_tenant_id')) { $query->where('tenant_id', session('impersonate_tenant_id'));} else { $query->where('tenant_id', auth()->user()->tenant_id);}
Tenant switching should only be available to super admins and must be audited. Log every tenant switch for security.
Every query must filter by tenant_id to prevent data leaks:
// Use global scopes via BelongsToTenant traitProduct::query()->get();// Or explicitly filterProduct::query()->where('tenant_id', auth()->user()->tenant_id)->get();
Validate tenant ownership in policies
Always verify tenant ownership in authorization:
public function update(User $user, Product $product): bool{ if ($product->tenant_id !== $user->tenant_id) { return false; } return $user->hasPermission('products.edit');}
Never expose tenant_id in URLs
Don’t use /tenants/{tenant_id}/products. Use:
/products - Tenant is inferred from authenticated user