mikailfaruqali / tenancy
Fast file-based multi-tenancy package for Laravel with subdomain-based tenant identification, automatic database isolation, dynamic connection switching, and seamless tenant management interface.
Installs: 26
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/mikailfaruqali/tenancy
Requires
- php: ^8.2
- illuminate/console: >=11.0
- illuminate/contracts: >=11.0
- illuminate/database: >=11.0
- illuminate/http: >=11.0
- illuminate/support: >=11.0
Requires (Dev)
- driftingly/rector-laravel: ^2.0
- laravel/pint: ^1.14
- orchestra/testbench: ^8.22 || ^9.0
README
A powerful yet simple Laravel package for database-per-tenant multi-tenancy with subdomain-based tenant identification. Fast file-based tenant registry, automatic database isolation, and dynamic connection switching.
📖 Table of Contents
- Features
- Quick Start
- Requirements
- Installation
- Configuration
- Basic Setup
- Usage
- Helper Functions
- Artisan Commands
- Advanced Configuration
- Management Interface
- Middleware
- Exception Handling
- API Reference
- Common Patterns
- Security Considerations
- Troubleshooting
- Contributing
- License
✨ Features
- 🗄️ Database-Per-Tenant - Complete database isolation for each tenant
- 🌐 Subdomain-Based - Automatic tenant identification via subdomains
- ⚡ Fast File Storage - JSON-based tenant registry for quick access
- 🔐 Secure Isolation - Separate MySQL users and databases per tenant
- 🎨 Management Interface - Web UI for tenant management with health monitoring
- 🔍 Health Monitoring - Built-in tenant health checks with customization
- 🛠️ Artisan Commands - CLI tools for creating, deleting, and upgrading tenants
- 🎯 Middleware Support - Automatic tenant detection and connection switching
- 🔧 Highly Customizable - Hooks for custom connection, migration, and health check logic
🚀 Quick Start
# Install the package composer require mikailfaruqali/tenancy # Publish configuration php artisan vendor:publish --tag=snawbar-tenancy-config # Create storage directory mkdir storage/tenancy # Configure .env echo "TENANCY_ENABLED=true" >> .env echo "TENANCY_DOMAIN=yourdomain.com" >> .env echo "TENANCY_MAIN_DOMAIN=yourdomain.com" >> .env echo "TENANCY_DB_USERNAME=root" >> .env echo "TENANCY_DB_PASSWORD=your_password" >> .env # Setup connection in AppServiceProvider (see Basic Setup section) # Create your first tenant php artisan tenancy:create
📋 Requirements
- PHP 8.2 or higher
- Laravel 11.0 or higher
- MySQL database
- MySQL user with
CREATE DATABASE,CREATE USER, andGRANTprivileges
📦 Installation
1. Install via Composer
composer require mikailfaruqali/tenancy
2. Publish Configuration
php artisan vendor:publish --tag=snawbar-tenancy-config
This will create config/snawbar-tenancy.php.
3. Publish Views (Optional)
php artisan vendor:publish --tag=snawbar-tenancy-views
This will publish views to resources/views/vendor/snawbar-tenancy/.
4. Configure Environment
Add to your .env file:
# Enable/disable multi-tenancy TENANCY_ENABLED=true # Main domain for tenant subdomains TENANCY_DOMAIN=yourdomain.com # Main domain (where admin panel is accessible) TENANCY_MAIN_DOMAIN=yourdomain.com # MySQL user that can access all tenant databases (for admin operations) TENANCY_MAIN_DOMAIN_OWNER=root # MySQL root credentials (for creating/deleting databases) TENANCY_DB_HOST=127.0.0.1 TENANCY_DB_PORT=3306 TENANCY_DB_USERNAME=root TENANCY_DB_PASSWORD=your_root_password
5. Create Storage Directory
The package stores tenant information in a JSON file. Create the directory:
mkdir -p storage/tenancy
Or create it manually on Windows:
mkdir storage\tenancy
The package will create storage/tenancy/tenants.json automatically when you create your first tenant. Ensure this directory has write permissions.
Important: Keep this file backed up as it contains all tenant database credentials. You may also want to add it to .gitignore if it contains sensitive information, though typically it should be version controlled in secure environments.
⚙️ Configuration
Main Configuration (config/snawbar-tenancy.php)
<?php return [ // Enable or disable multi-tenancy 'enabled' => env('TENANCY_ENABLED', false), // Domain for tenant subdomains (tenant1.yourdomain.com) 'domain' => env('TENANCY_DOMAIN', 'localhost'), // Main domain where admin panel is accessible 'main_domain' => env('TENANCY_MAIN_DOMAIN'), // MySQL user with access to all tenant databases 'main_domain_owner' => env('TENANCY_MAIN_DOMAIN_OWNER', 'root'), // Path to tenants.json file 'storage_path' => storage_path('tenancy/tenants.json'), // Path to upgrade SQL file 'upgrade_sql_path' => storage_path('tenancy/upgrade.sql'), // Database configuration for tenant management 'database' => [ 'driver' => 'mysql', 'host' => env('TENANCY_DB_HOST', '127.0.0.1'), 'port' => env('TENANCY_DB_PORT', '3306'), 'username' => env('TENANCY_DB_USERNAME', 'root'), 'password' => env('TENANCY_DB_PASSWORD', ''), ], // Sort options for the management UI // These correspond to keys in your health check callback response 'health_sort_options' => [ // Example: 'journals' => 'Most Journals', // Example: 'invoices' => 'Most Invoices', ], ];
🚀 Basic Setup
1. Register Service Provider
The service provider is auto-discovered by Laravel. It will automatically register:
- Configuration files
- Routes
- Views
- Artisan commands (tenancy:create, tenancy:delete, tenancy:upgrade)
- Helper functions
2. Configure Connection Handler
In your AppServiceProvider or a dedicated service provider:
use Illuminate\Support\Facades\DB; use Snawbar\Tenancy\Facades\Tenancy; public function boot(): void { // Define how to connect to a tenant database Tenancy::connectUsing(function ($credentials) { config([ 'database.connections.tenant' => [ 'driver' => 'mysql', 'host' => config('database.connections.mysql.host'), 'port' => config('database.connections.mysql.port'), 'database' => $credentials->database, 'username' => $credentials->username, 'password' => $credentials->password, 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', ], ]); DB::setDefaultConnection('tenant'); DB::reconnect('tenant'); }); }
3. Configure Migration Handler
Tenancy::migrateUsing(function ($command = null) { $command?->call('migrate', [ '--database' => 'tenant', '--force' => true, ]); });
4. Register Middleware
In bootstrap/app.php (Laravel 11):
use Snawbar\Tenancy\Middleware\InitializeTenancy; use Snawbar\Tenancy\Middleware\EnsureMainTenancy; ->withMiddleware(function (Middleware $middleware) { // Apply to web routes for automatic tenant detection $middleware->web(append: [ InitializeTenancy::class, ]); // Apply to admin routes to ensure they only work on main domain $middleware->alias([ 'main-tenancy' => EnsureMainTenancy::class, ]); })
Or in app/Http/Kernel.php (Laravel 10):
protected $middlewareGroups = [ 'web' => [ // ... other middleware \Snawbar\Tenancy\Middleware\InitializeTenancy::class, ], ]; protected $middlewareAliases = [ // ... other aliases 'main-tenancy' => \Snawbar\Tenancy\Middleware\EnsureMainTenancy::class, ];
🎯 Usage
Creating Tenants
Via Artisan Command
php artisan tenancy:create
You'll be prompted for:
- Tenant name (alphanumeric and hyphens only, will be sanitized to database-safe format)
- MySQL root password
The command will:
- Create a new MySQL database (e.g.,
company_name_db) - Create a dedicated MySQL user for the tenant (e.g.,
company_name_usr) - Grant privileges to the tenant user and main domain owner
- Run migrations on the tenant database
- Store tenant information in
storage/tenancy/tenants.json
Note: Tenant names are automatically sanitized:
- Converted to lowercase
- Non-alphanumeric characters replaced with underscores
- Truncated to 16 characters for database compatibility
Via Code
use Snawbar\Tenancy\Facades\Tenancy; // Create tenant $tenant = Tenancy::create('company-name', 'mysql_root_password'); // The tenant object contains: // - subdomain: "company-name.yourdomain.com" // - database: { database, username, password } // Run migrations Tenancy::migrate($tenant);
Via Management Interface
Access the web interface at your main domain:
https://yourdomain.com/snawbar-tenancy/list-view- List all tenantshttps://yourdomain.com/snawbar-tenancy/create-view- Create new tenant
Deleting Tenants
Via Artisan Command
php artisan tenancy:delete
Select a tenant from the list and provide the MySQL root password.
Via Code
$tenant = Tenancy::findOrFail('company-name.yourdomain.com'); Tenancy::delete($tenant, 'mysql_root_password');
Upgrading Tenants
When you need to run SQL updates across all tenants:
- Create
storage/tenancy/upgrade.sqlwith your SQL:
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
- Run the upgrade command:
php artisan tenancy:upgrade
This will:
- Connect to each tenant database
- Execute the SQL file
- Run any custom upgrade logic
- Delete the SQL file after completion
Finding Tenants
// Get all tenants $tenants = Tenancy::all(); // Find specific tenant $tenant = Tenancy::find('company.yourdomain.com'); // Find or throw exception $tenant = Tenancy::findOrFail('company.yourdomain.com'); // Check if tenant exists if (Tenancy::exists('company.yourdomain.com')) { // ... }
Manual Connection Switching
// Connect by subdomain Tenancy::connectWithSubdomain('company.yourdomain.com'); // Connect with credentials object Tenancy::connectWithCredentials($tenant->database);
Health Monitoring
// Check single tenant health $health = Tenancy::health($tenant); // Returns: ['status' => 'active', 'record_count' => 1250, 'table_count' => 15] // Get all tenants with health data $tenants = Tenancy::withHealth();
🛠️ Helper Functions
The package includes a helper function for formatting health check values in views:
formatHealthValue($value): string
This function formats health values appropriately:
- Numbers: Formatted with commas (e.g., 1000 → 1,000)
- Dates: Formatted as Y-m-d (e.g., 2026-01-10)
- Other values: Returned as-is
Used in the management interface to display health metrics cleanly.
🎮 Artisan Commands
The package provides three Artisan commands for tenant management:
tenancy:create
Creates a new tenant with database, user, and runs migrations.
php artisan tenancy:create
Interactive prompts:
- Tenant name (validated: lowercase letters, numbers, hyphens)
- MySQL root password
What it does:
- Validates tenant name format
- Creates MySQL database and user
- Grants appropriate privileges
- Runs migrations on tenant database
- Saves tenant to registry
tenancy:delete
Deletes an existing tenant and all associated resources.
php artisan tenancy:delete
Interactive prompts:
- Select tenant from searchable list
- MySQL root password
What it does:
- Drops the tenant database
- Drops the tenant MySQL user
- Removes tenant from registry
- Executes any
afterDeleteUsing()hooks
tenancy:upgrade
Runs SQL updates and custom logic across all tenants.
php artisan tenancy:upgrade
Usage:
- Create
storage/tenancy/upgrade.sqlwith your SQL statements - Run the command
- The SQL is executed on each tenant database
- Custom
afterUpgradeUsing()hooks are executed - The upgrade.sql file is automatically deleted
Example upgrade.sql:
ALTER TABLE users ADD COLUMN phone VARCHAR(20); CREATE INDEX idx_users_email ON users(email);
🔧 Advanced Configuration
All configuration hooks should be registered in a service provider's boot() method, typically in AppServiceProvider.
Custom Connection Logic
Define how to connect to tenant databases. This is required for the package to function.
Tenancy::connectUsing(function ($credentials) { // Your custom connection logic // e.g., connect to different database servers based on tenant config([ 'database.connections.tenant' => [ 'driver' => 'mysql', 'host' => config('database.connections.mysql.host'), 'port' => config('database.connections.mysql.port'), 'database' => $credentials->database, 'username' => $credentials->username, 'password' => $credentials->password, ], ]); DB::setDefaultConnection('tenant'); DB::reconnect('tenant'); });
After Connection Hook
Tenancy::afterConnectUsing(function ($request) { // Run after tenant connection is established // e.g., set up tenant-specific configuration Log::info('Connected to tenant: ' . $request->getHost()); });
Custom Health Checks
use Illuminate\Database\Connection; Tenancy::healthUsing(function (Connection $connection) { // Return custom health metrics return [ 'status' => 'active', 'users_count' => $connection->table('users')->count(), 'posts_count' => $connection->table('posts')->count(), 'last_activity' => $connection->table('activity_logs')->max('created_at'), ]; });
Note: If you want to make these metrics sortable in the management UI, add them to the health_sort_options array in your config file:
// config/snawbar-tenancy.php 'health_sort_options' => [ 'users_count' => 'Most Users', 'posts_count' => 'Most Posts', 'last_activity' => 'Recent Activity', ],
After Upgrade Hook
Tenancy::afterUpgradeUsing(function ($tenant, $command) { $command->info("Running custom upgrade for {$tenant->subdomain}"); // Custom upgrade logic per tenant });
After Delete Hook
Tenancy::afterDeleteUsing(function ($subdomain, $command) { // Cleanup logic after tenant deletion Storage::disk('s3')->deleteDirectory("tenants/{$subdomain}"); });
Custom Main Domain Validation
Tenancy::ensureMainTenantUsing(function ($request) { // Custom logic to determine if request is on main domain return $request->getHost() === 'admin.yourdomain.com'; });
🎨 Management Interface
The package includes a beautiful web interface for managing tenants.
Routing Setup
The routes are automatically registered with the main-tenancy middleware to ensure they're only accessible on the main domain. To add authentication, you can modify the routes in your application by re-registering them:
// In routes/web.php use Snawbar\Tenancy\Controllers\TenancyController; use Snawbar\Tenancy\Middleware\EnsureMainTenancy; Route::middleware(['auth', EnsureMainTenancy::class]) ->prefix('snawbar-tenancy') ->name('tenancy.') ->group(function () { Route::get('list-view', [TenancyController::class, 'listView'])->name('list.view'); Route::get('create-view', [TenancyController::class, 'createView'])->name('create.view'); Route::post('create', [TenancyController::class, 'create'])->name('create'); });
Available Routes
GET /snawbar-tenancy/list-view- List all tenants with health status, search, and sortingGET /snawbar-tenancy/create-view- Create new tenant formPOST /snawbar-tenancy/create- Handle tenant creation
Features
- 📊 Real-time health monitoring
- 🔍 Search tenants by subdomain
- 📈 Sort by database usage or custom health metrics
- 📄 Pagination support
- ⚡ Ajax-based tenant creation
- 🎨 Modern, responsive UI
Customizing Views
If you want to customize the management interface, publish the views:
php artisan vendor:publish --tag=snawbar-tenancy-views
This will publish three Blade views to resources/views/vendor/snawbar-tenancy/:
index.blade.php- List tenants viewcreate.blade.php- Create tenant form404.blade.php- Tenant not found error page
You can then modify these views to match your application's design.
�️ Middleware
InitializeTenancy
Automatically detects tenant from subdomain and switches database connection.
// Applied to web middleware group public function handle(Request $request, Closure $next) { if (config('snawbar-tenancy.enabled')) { Tenancy::connectWithSubdomain($request->getHost()); } return $next($request); }
EnsureMainTenancy
Ensures routes are only accessible on the main domain (admin panel).
Route::middleware('main-tenancy')->group(function () { // Only accessible on main domain Route::get('/admin', ...); });
🎭 Exception Handling
TenancyNotFound
Thrown when a tenant subdomain doesn't exist. Automatically renders a beautiful 404 page.
try { $tenant = Tenancy::findOrFail('nonexistent.yourdomain.com'); } catch (TenancyNotFound $e) { // Handled automatically with custom 404 view }
TenancyAlreadyExists
Thrown when trying to create a tenant that already exists.
try { Tenancy::create('existing-tenant'); } catch (TenancyAlreadyExists $e) { // Handle duplicate tenant }
TenancyDatabaseException
Thrown when database operations fail.
try { Tenancy::create('new-tenant', 'wrong_password'); } catch (TenancyDatabaseException $e) { Log::error($e->getMessage()); }
📚 API Reference
Facade Methods
Configuration Hooks
Tenancy::connectUsing(Closure $callback): void Tenancy::migrateUsing(Closure $callback): void Tenancy::healthUsing(Closure $callback): void Tenancy::ensureMainTenantUsing(Closure $callback): void Tenancy::afterConnectUsing(Closure $callback): void Tenancy::afterUpgradeUsing(Closure $callback): void Tenancy::afterDeleteUsing(Closure $callback): void
Runtime API
Tenancy::all(): Collection Tenancy::find(string $subdomain): ?object Tenancy::findOrFail(string $subdomain): object Tenancy::exists(string $subdomain): bool Tenancy::health(object $tenant): array Tenancy::withHealth(): Collection
Connection & Migration
Tenancy::connectWithSubdomain(string $subdomain): void Tenancy::connectWithCredentials(object $credentials): void Tenancy::migrate(object $tenant, ?Command $command = null): void
Tenant Lifecycle
Tenancy::create(string $name, ?string $rootPassword = null): object Tenancy::delete(object $tenant, ?string $rootPassword = null): void
Tenant Object Structure
{
"subdomain": "company.yourdomain.com",
"database": {
"database": "company_db",
"username": "company_usr",
"password": "random_16_char_password"
}
}
🎯 Common Patterns
Multi-Database Queries
Get data from all tenants by iterating and connecting to each:
$allData = Tenancy::all()->map(function ($tenant) { Tenancy::connectWithCredentials($tenant->database); return [ 'subdomain' => $tenant->subdomain, 'users_count' => DB::table('users')->count(), 'orders_count' => DB::table('orders')->count(), ]; });
Tenant-Specific Configuration
Set configuration based on tenant after connection:
Tenancy::afterConnectUsing(function ($request) { $tenant = Tenancy::find($request->getHost()); if ($tenant) { // Set tenant-specific config config([ 'app.name' => $tenant->name ?? config('app.name'), 'mail.from.name' => $tenant->email ?? config('mail.from.name'), ]); } });
Background Jobs for Tenants
Process background jobs for specific tenants:
// Dispatch a job for a specific tenant dispatch(function () use ($tenant) { Tenancy::connectWithCredentials($tenant->database); // Your tenant-specific job logic User::where('status', 'inactive')->delete(); })->delay(now()->addHours(1));
});
## 🔒 Security Considerations
1. **Tenant Isolation**: Each tenant has a dedicated database and MySQL user with access only to their database
2. **Credential Storage**: Tenant credentials are stored in `storage/tenancy/tenants.json` - ensure proper file permissions
3. **Root Password**: MySQL root password is only used during tenant creation/deletion, never stored
4. **Subdomain Validation**: Tenant names are sanitized to alphanumeric and hyphens only
5. **Main Domain Protection**: Use `EnsureMainTenancy` middleware to protect admin routes
## 🐛 Troubleshooting
### Tenant Not Found
**Issue**: Getting 404 when accessing tenant subdomain
**Solutions**:
- Ensure DNS wildcard record `*.yourdomain.com` points to your server
- Check `TENANCY_ENABLED=true` in `.env`
- Verify tenant exists: `php artisan tinker` → `Tenancy::all()`
### Connection Not Switching
**Issue**: Still connected to main database when accessing tenant
**Solutions**:
- Verify `connectUsing()` callback is registered in `AppServiceProvider`
- Ensure `InitializeTenancy` middleware is applied to web routes
- Check connection is being set: `DB::getDefaultConnection()`
### Migration Fails
**Issue**: Migrations don't run on tenant database
**Solutions**:
- Verify `migrateUsing()` callback is configured
- Ensure migrations exist in `database/migrations/`
- Check MySQL user has proper permissions
- Run manually: `Tenancy::migrate($tenant)`
### Permission Denied
**Issue**: Cannot create database or user
**Solutions**:
- Verify MySQL root credentials in `.env`
- Ensure MySQL user has `CREATE DATABASE`, `CREATE USER`, `GRANT` privileges
- Test connection: `mysql -u root -p`
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## 📄 License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
## 👨💻 Author
**Snawbar**
- Email: alanfaruq85@gmail.com
- GitHub: [@mikailfaruqali](https://github.com/mikailfaruqali)
## 🙏 Acknowledgments
Built with ❤️ for the Laravel community.
---
**Need Help?** Open an issue on [GitHub](https://github.com/mikailfaruqali/tenancy/issues)