PHPackages                             roberts/laravel-singledb-tenancy - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. roberts/laravel-singledb-tenancy

ActiveLibrary[Framework](/categories/framework)

roberts/laravel-singledb-tenancy
================================

Laravel package for single database multi-tenancy

v1.0.0(6mo ago)0975[1 PRs](https://github.com/roberts/laravel-singledb-tenancy/pulls)1MITPHPPHP ^8.4CI passing

Since Oct 21Pushed 1mo agoCompare

[ Source](https://github.com/roberts/laravel-singledb-tenancy)[ Packagist](https://packagist.org/packages/roberts/laravel-singledb-tenancy)[ Docs](https://github.com/roberts/laravel-singledb-tenancy)[ GitHub Sponsors](https://github.com/drewroberts)[ RSS](/packages/roberts-laravel-singledb-tenancy/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (21)Versions (7)Used By (1)

Laravel Single Database Tenancy
===============================

[](#laravel-single-database-tenancy)

[![Latest Version on Packagist](https://camo.githubusercontent.com/53e2c08ac818386fe98071385f2f7973a28d50fede3c7a77bcfc7f49e6d30f6a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f726f62657274732f6c61726176656c2d73696e676c6564622d74656e616e63792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/roberts/laravel-singledb-tenancy)[![GitHub Tests Action Status](https://camo.githubusercontent.com/4f006c41a7e75ce932f4b555059b24a78f6510bd203a9388865cc4f5e92768cb/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f62657274732f6c61726176656c2d73696e676c6564622d74656e616e63792f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/roberts/laravel-singledb-tenancy/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/8f47a61c2888e2dce0398c848a29f4b58620d28cddceee51614d65b22317329c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f62657274732f6c61726176656c2d73696e676c6564622d74656e616e63792f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/roberts/laravel-singledb-tenancy/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/52fedf92f1d662afdbede7c48970ec91a73dde6312a0fb408355c8956552dc29/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f726f62657274732f6c61726176656c2d73696e676c6564622d74656e616e63792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/roberts/laravel-singledb-tenancy)

A Laravel package for single-database multi-tenancy. It offers automatic data isolation, tenant resolution by domain, and flexible routing, making it a complete solution for SaaS applications.

Features
--------

[](#features)

- **Automatic Tenant Resolution**: Resolve tenants by domain or subdomain.
- **Data Isolation**: Automatically scope Eloquent models to the current tenant.
- **Tenant Context**: Global helper functions (`current_tenant()`) to access the active tenant.
- **Smart Fallback**: Automatically fall back to a primary tenant if no tenant is resolved.
- **Caching**: Fast tenant resolution via configurable caching.
- **Tenant-Specific Routing**: Support for loading custom route files for each tenant.
- **Artisan Commands**: Built-in commands for migrations and diagnostics.
- **Forced Tenant Mode**: Simplify development and testing by forcing a specific tenant.

Requirements
------------

[](#requirements)

- PHP 8.4+
- Laravel 12.0+

Installation
------------

[](#installation)

Install the package via Composer:

```
composer require roberts/laravel-singledb-tenancy
```

Publish and run the migrations:

```
php artisan vendor:publish --tag="laravel-singledb-tenancy-migrations"
php artisan migrate
```

Publish the configuration file:

```
php artisan vendor:publish --tag="laravel-singledb-tenancy-config"
```

Configuration
-------------

[](#configuration)

The package provides extensive configuration options in `config/singledb-tenancy.php`:

### Caching Configuration

[](#caching-configuration)

```
'caching' => [
    'enabled' => env('TENANT_CACHE_ENABLED', true),
    'store' => env('TENANT_CACHE_STORE', 'array'),
    'ttl' => env('TENANT_CACHE_TTL', 3600),
],
```

### Error Handling

[](#error-handling)

```
'failure_handling' => [
    'unresolved_tenant' => 'fallback', // fallback|continue|exception|redirect
],
```

Basic Usage
-----------

[](#basic-usage)

### 1. Creating Tenants

[](#1-creating-tenants)

```
use Roberts\LaravelSingledbTenancy\Models\Tenant;

// Create a tenant with a root domain
$tenant = Tenant::create([
    'name' => 'Acme Corporation',
    'domain' => 'acme.com',
    'slug' => 'acme', // Auto-generated if not provided
]);

// Create a tenant with a subdomain
$tenant = Tenant::create([
    'name' => 'Beta Company',
    'domain' => 'beta.acme.com', // Full subdomain in domain field
    'slug' => 'beta',
]);

// Create another tenant with a different subdomain
$tenant = Tenant::create([
    'name' => 'Enterprise Division',
    'domain' => 'enterprise.acme.com', // Each subdomain gets its own entry
    'slug' => 'enterprise',
]);
```

### 2. Making Models Tenant-Aware

[](#2-making-models-tenant-aware)

Add the `HasTenant` trait to your models. It automatically scopes queries to the current tenant, sets the `tenant_id` on creation, and adds a `tenant()` relationship.

```
use Roberts\LaravelSingledbTenancy\Traits\HasTenant;

class Post extends Model
{
    use HasTenant;

    protected $fillable = ['tenant_id'];
}
```

This automatically applies tenant scoping to queries, sets tenant\_id on creation, and provides a `tenant()` relationship.

### 3. Setting Up Middleware

[](#3-setting-up-middleware)

Apply the tenant resolution middleware to your routes:

```
// Domain resolution for all routes
Route::middleware(['web', 'tenant'])->group(function () {
    Route::get('/dashboard', DashboardController::class);
});

// Domain resolution only
Route::middleware(['web', 'tenant:domain'])->group(function () {
    Route::get('/custom', CustomController::class);
});
```

Advanced Features
-----------------

[](#advanced-features)

### Tenant-Aware Jobs

[](#tenant-aware-jobs)

To make your queued jobs tenant-aware, simply use the `TenantAware` trait. It automatically captures the tenant context when the job is dispatched and restores it when the job is processed.

```
use Roberts\LaravelSingledbTenancy\Concerns\TenantAware;

class ProcessReport implements ShouldQueue
{
    use TenantAware;

    public function handle()
    {
        // All Eloquent queries are automatically scoped to the correct tenant.
        $sales = Sale::all();
    }
}
```

### Tenant-Aware Commands

[](#tenant-aware-commands)

Create tenant-aware artisan commands by extending the `TenantAwareCommand` class. This automatically provides `--tenant=` and `--all-tenants` options.

Implement your logic in the `handleTenant()` method, where the tenant context is guaranteed to be set.

```
use Roberts\LaravelSingledbTenancy\Commands\TenantAwareCommand;

class GenerateReport extends TenantAwareCommand
{
    protected $signature = 'report:generate {--tenant=} {--all-tenants}';
    protected $description = 'Generate a sales report.';

    public function handleTenant()
    {
        $this->info('Generating report for: ' . current_tenant()->name);
    }
}
```

### Tenant Lifecycle Events

[](#tenant-lifecycle-events)

The package dispatches events to allow you to hook into the tenant lifecycle:

- `TenantCreated`: After a new tenant is created.
- `TenantResolved`: After the tenant context is set for a request.
- `TenantSuspended`: After a tenant is suspended.
- `TenantReactivated`: After a tenant is reactivated.
- `TenantDeleted`: After a tenant is soft deleted.

You can listen for these events in your `EventServiceProvider`:

```
use Roberts\LaravelSingledbTenancy\Events\TenantCreated;

protected $listen = [
    TenantCreated::class => [
        'App\Listeners\SetupNewTenant',
    ],
];
```

### Tenant Context Management

[](#tenant-context-management)

The package provides global helper functions for tenant context:

```
// Get current tenant
$tenant = current_tenant();
$tenantId = current_tenant_id();

// Check if tenant is set
if (has_tenant()) {
    // Tenant-specific logic
}

// Require tenant (throws exception if none set)
$tenant = require_tenant();

// Run code in specific tenant context
tenant_context()->runWith($tenant, function () {
    // This code runs with $tenant as current tenant
    $posts = Post::all(); // Only posts for $tenant
});
```

### Manual Tenant Context

[](#manual-tenant-context)

You can manually set the tenant context:

```
// Set tenant context
tenant_context()->set($tenant);

// Clear tenant context
tenant_context()->clear();

// Run without tenant context (see all data)
tenant_context()->runWithout(function () {
    $allPosts = Post::all(); // All posts across all tenants
});
```

### Model Scoping

[](#model-scoping)

Tenant-aware models are automatically scoped:

```
// Automatically scoped to current tenant
$posts = Post::all();

// Query specific tenant
$posts = Post::forTenant($tenant)->get();

// Query all tenants (removes tenant scope)
$allPosts = Post::forAllTenants()->get();

// Custom tenant column (override default 'tenant_id')
class CustomModel extends Model
{
    use HasTenant;

    protected $tenantColumn = 'organization_id';
}
```

### Custom Route Files

[](#custom-route-files)

You can create tenant-specific route files in the `routes/tenants/` directory. The file name should match the tenant's domain.

```
routes/
├── web.php              # Default routes for all tenants
└── tenants/
    ├── acme.com.php         # Routes for 'acme.com' tenant domain
    └── sub.acme.com.php   # Routes for 'sub.acme.com' tenant domain

```

**Important:** When a custom route file is found for a tenant, it **overrides** the default `routes/web.php` file. If you want to augment the default routes, you must manually include them at the botttom of your tenant's route file:

```
// routes/tenants/acme.com.php
Route::get('/special', ...);

// Also load all the shared routes
require base_path('routes/web.php');
```

### Development and Testing

[](#development-and-testing)

Force a specific tenant during development:

```
# .env
FORCE_TENANT_DOMAIN=dev.yourapp.com
```

Disable tenant resolution for tests that need to see all data:

```
tenant_context()->runWithout(function () {
    $this->assertCount(10, Post::all()); // All tenant data
});
```

Tenant Resolution
-----------------

[](#tenant-resolution)

The package uses **domain-based resolution** to match the full request domain against the `domain` column in your tenants table. This works for both root domains and subdomains.

### How It Works

[](#how-it-works)

```
// Root domain resolution
// Request: https://acme.com/dashboard
// Matches: Tenant with domain = 'acme.com'

// Subdomain resolution
// Request: https://beta.acme.com/dashboard
// Matches: Tenant with domain = 'beta.acme.com'

// Deep subdomain resolution
// Request: https://api.beta.acme.com/dashboard
// Matches: Tenant with domain = 'api.beta.acme.com'

// Custom domain resolution
// Request: https://customdomain.co.uk/dashboard
// Matches: Tenant with domain = 'customdomain.co.uk'
```

### Database Structure

[](#database-structure)

Your tenants table should contain complete domain entries:

```
// Example tenant records:
['id' => 1, 'name' => 'Main Site', 'domain' => 'acme.com', 'slug' => 'main']
['id' => 2, 'name' => 'Beta Site', 'domain' => 'beta.acme.com', 'slug' => 'beta']
['id' => 3, 'name' => 'API Site', 'domain' => 'api.acme.com', 'slug' => 'api']
['id' => 4, 'name' => 'Enterprise', 'domain' => 'enterprise.acme.com', 'slug' => 'enterprise']
['id' => 5, 'name' => 'Custom Domain', 'domain' => 'anotherdomain.com', 'slug' => 'another']
```

If no tenant is resolved, the Smart Fallback Logic can automatically fallback to a designated primary tenant.

Smart Fallback Logic
--------------------

[](#smart-fallback-logic)

The Smart Fallback Logic provides automatic fallback to a primary tenant when normal resolution fails. This ensures your application always has a valid tenant context, which is particularly useful for shared content or landing pages.

### How It Works

[](#how-it-works-1)

1. **Normal Resolution**: First attempts standard domain/subdomain resolution
2. **Fallback Check**: If no tenant is found and fallback is enabled, checks for primary tenant
3. **Primary Tenant**: Falls back to tenant with ID 1 (configurable)
4. **Smart Skipping**: Automatically skips fallback when no tenants exist in the database
5. **Suspension Respect**: Won't fallback to suspended primary tenant

### Caching Behavior

[](#caching-behavior)

- Primary tenant existence is cached permanently once confirmed
- Cache is invalidated when tenants are deleted
- Tenant existence cache prevents unnecessary database queries

### Use Cases

[](#use-cases)

- **Landing Pages**: Serve shared content when no tenant is specified
- **Marketing Sites**: Display default content for non-tenant visitors
- **Development**: Consistent behavior during application setup
- **Error Recovery**: Graceful handling of misconfigured domains

Management Commands
-------------------

[](#management-commands)

The package includes helpful Artisan commands for managing your tenancy setup:

### Add Tenant Column Command

[](#add-tenant-column-command)

Quickly add tenant\_id columns to existing tables with proper foreign key constraints:

```
# Add tenant_id column to posts table
php artisan tenancy:add-tenant-column posts

# Add with custom options
php artisan tenancy:add-tenant-column posts --nullable --index --column=organization_id
```

### Tenancy Info Command

[](#tenancy-info-command)

Display comprehensive information about your tenancy configuration and current state:

```
php artisan tenancy:info
```

This command shows:

- Resolution strategy status
- Caching configuration
- Current tenant context
- Database tenant statistics
- Smart Fallback Logic settings

### Caching

[](#caching)

Tenant resolution results are cached automatically to improve performance. Cache is invalidated when tenants are modified.

Error Handling
--------------

[](#error-handling-1)

### Unresolved Tenant

[](#unresolved-tenant)

When no tenant can be resolved from the request:

- `fallback` - Use Smart Fallback Logic to primary tenant
- `continue` - Continue without tenant context
- `exception` - Throw RuntimeException
- `redirect` - Redirect to specified route

Suspended (soft deleted) tenants are automatically blocked and will not be resolved.

Security
--------

[](#security)

### Super Admin

[](#super-admin)

You can designate a single "super admin" user who has privileges over the entire tenancy system (e.g., for accessing a future admin panel). This is configured by setting an environment variable:

```
# .env
TENANCY_SUPER_ADMIN_EMAIL=super@admin.com
```

The package provides a `SuperAdmin` service to check if a user is the designated super admin:

```
use Roberts\LaravelSingledbTenancy\Services\SuperAdmin;

$user = auth()->user();

if (app(SuperAdmin::class)->is($user)) {
    // User is the super admin
}
```

### Primary Tenant Authorization

[](#primary-tenant-authorization)

The package includes a middleware to restrict access to routes that should only be available on the primary tenant's domain (i.e., the tenant with ID `1`). This is useful for creating a centralized admin panel.

To use it, simply add the `auth.primary` middleware to your routes:

```
use Roberts\LaravelSingledbTenancy\Middleware\AuthorizePrimaryTenant;

Route::get('/tenancy-dashboard', ...)->middleware(AuthorizePrimaryTenant::class);
```

If a user attempts to access this route from any domain other than the primary tenant's, they will receive a `404 Not Found` error.

Testing
-------

[](#testing)

Run the comprehensive test suite:

```
composer test                # Run tests
composer test:coverage       # Run with coverage
composer analyse             # Static analysis
```

Test your tenant-aware code:

```
use Roberts\LaravelSingledbTenancy\Models\Tenant;

class PostTest extends TestCase
{
    public function test_posts_are_scoped_to_tenant()
    {
        $tenant1 = Tenant::factory()->create();
        $tenant2 = Tenant::factory()->create();

        tenant_context()->set($tenant1);
        $post1 = Post::create(['title' => 'Tenant 1 Post']);

        tenant_context()->set($tenant2);
        $post2 = Post::create(['title' => 'Tenant 2 Post']);

        // Verify isolation
        tenant_context()->set($tenant1);
        $this->assertCount(1, Post::all());
        $this->assertEquals('Tenant 1 Post', Post::first()->title);
    }
}
```

API Reference
-------------

[](#api-reference)

### Models

[](#models)

#### Tenant

[](#tenant)

```
// Properties
$tenant->id;           // Primary key
$tenant->name;         // Tenant display name
$tenant->slug;         // URL-safe identifier
$tenant->domain;       // Custom domain (optional)
$tenant->suspended_at; // Soft delete timestamp

// Methods
$tenant->isActive();                    // Check if tenant is active
$tenant->suspend();                     // Suspend tenant
$tenant->reactivate();                  // Reactivate tenant
$tenant->url($path = '/');              // Generate tenant URL
Tenant::resolveByDomain($domain);       // Find tenant by domain
Tenant::resolveBySlug($slug);           // Find tenant by slug
```

### Services

[](#services)

#### TenantContext

[](#tenantcontext)

```
// Set/get current tenant
tenant_context()->set($tenant);
$tenant = tenant_context()->get();
tenant_context()->clear();

// Run code in tenant context
tenant_context()->runWith($tenant, $callback);
tenant_context()->runWithout($callback);

// Check tenant state
tenant_context()->has();
tenant_context()->id();
```

#### TenantCache

[](#tenantcache)

Automatic caching of tenant resolution - no direct usage required.

### Middleware

[](#middleware)

#### TenantResolutionMiddleware

[](#tenantresolutionmiddleware)

```
// Apply to routes
Route::middleware('tenant')->group(...);           // All strategies
Route::middleware('tenant:domain')->group(...);    // Domain only
Route::middleware('tenant:subdomain')->group(...); // Subdomain only
```

Configuration Reference
-----------------------

[](#configuration-reference)

### Full Configuration File

[](#full-configuration-file)

```
