PHPackages                             nuelcyoung/tenantable - 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. nuelcyoung/tenantable

ActiveLibrary[Framework](/categories/framework)

nuelcyoung/tenantable
=====================

Multi tenancy package for CodeIgniter 4 with multiple identification strategies, bootstrap system, and lifecycle events

v1.5.1(1mo ago)011MITPHPPHP ^8.1

Since Feb 23Pushed 1mo agoCompare

[ Source](https://github.com/nuelcyoung/tenantable)[ Packagist](https://packagist.org/packages/nuelcyoung/tenantable)[ Docs](https://github.com/nuelcyoung/tenantable)[ RSS](/packages/nuelcyoung-tenantable/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (2)Versions (10)Used By (0)

Tenantable - Multitenant Package for CodeIgniter 4
==================================================

[](#tenantable---multitenant-package-for-codeigniter-4)

A robust multitenant package for CodeIgniter 4 that provides flexible tenant identification and automatic tenant isolation.

Features
--------

[](#features)

- **Flexible Tenant Identification** — Subdomain, domain, path, or request data
- **Multiple Isolation Strategies** — Row-level, table prefix, or database-per-tenant
- **Automatic Provisioning** — Database auto-created and migrated on tenant creation
- **Third-Party Migration Support** — Run Shield (or any package) migrations per-tenant automatically
- **Automatic Tenant Context** — Models automatically respect tenant boundaries
- **Superadmin Bypass** — Built-in support for platform admins
- **CLI Support** — Create tenants, scaffold migrations/models, fan-out commands

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

[](#requirements)

- PHP 8.1+
- CodeIgniter 4.0+

Architecture Options
--------------------

[](#architecture-options)

This package supports **3 isolation strategies**:

StrategyHow It WorksProsCons**tenant\_id**Shared tables with `tenant_id` columnSimple to implementRisk of leakage**Table Prefix**Separate tables per tenant (`tenant_1_students`)No leakage possibleMore complex setup**Separate DB**Different database per tenantComplete isolationMost complex---

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

[](#installation)

```
composer require nuelcyoung/tenantable
php spark tenants:install
```

`tenants:install` is interactive on first run. It asks for the base domain, isolation mode, and identification strategy; then publishes `app/Config/Tenantable.php`, wires `app/Config/Filters.php` and `app/Config/Events.php` idempotently, scaffolds `app/Database/Migrations/Tenant/`, and runs `tenants:setup` to create the central tenants tables. Re-running it is safe.

For non-interactive / CI installs:

```
php spark tenants:install --base-domain=example.com --mode=prefix --strategy=domain_or_subdomain --yes
```

Filter aliases (`tenant_subdomain`, `tenant_domain`, `tenant_domain_or_subdomain`, `tenant_path`, `tenant_request`, `tenant_security`, `identify_tenant`) are auto-registered through CI4's `Config\Registrar` discovery — you don't need to add them to `Config\Filters::$aliases` yourself.

For the full step-by-step walkthrough, see [SETUP.md](SETUP.md).

---

Tenant Identification
---------------------

[](#tenant-identification)

Tenantable identifies which tenant a request belongs to using **filters**. You choose your strategy by applying the corresponding filter to your routes.

### Available Strategies

[](#available-strategies)

Filter AliasClassIdentifies byExample`tenant` / `tenant_subdomain``SubdomainFilter`URL subdomain`acme.example.com``tenant_domain``DomainFilter`Custom domain (stored in `tenants.domain`)`acme.com``tenant_domain_or_subdomain``DomainOrSubdomainFilter`Domain first, falls back to subdomain`acme.com` or `acme.example.com``tenant_path``PathFilter`First URL path segment`/acme/dashboard``tenant_request``RequestDataFilter`Header, query param, or body field`X-Tenant-ID: acme`### How to Configure

[](#how-to-configure)

Register your chosen filter in `app/Config/Filters.php`:

```
// Option A: Apply globally
public array $globals = [
    'before' => [
        'tenant_subdomain' => ['except' => ['health', 'api/*']],
    ],
];

// Option B: Apply per route group (you can mix strategies)
// In Routes.php:
$routes->group('app', ['filter' => 'tenant_subdomain'], function ($routes) {
    // Web routes identified by subdomain
});
$routes->group('api', ['filter' => 'tenant_request'], function ($routes) {
    // API routes identified by header/query param
});
```

All filters are auto-registered by the package. The default `tenant` alias maps to `SubdomainFilter`.

---

Strategy 1: Table Prefix (Recommended)
--------------------------------------

[](#strategy-1-table-prefix-recommended)

**Best for**: Most applications. No tenant\_id leakage risks.

### How It Works

[](#how-it-works)

```
students table → tenant_1_students, tenant_2_students, ...
classes table → tenant_1_classes, tenant_2_classes, ...

```

### Setup

[](#setup)

1. **Run Migration**

```
php spark migrate -g tenantable
```

2. **Configure Filters**

```
// app/Config/Filters.php
public array $globals = [
    'before' => [
        'tenant_subdomain' => ['except' => ['health', 'api/*']],
    ],
];
```

3. **Use the Model**

```
use nuelcyoung\tenantable\Traits\TenantTablePrefixModel;

class StudentModel extends TenantTablePrefixModel
{
    protected $table = 'students';
}

// Automatic: queries tenant_1_students when tenant_id = 1
$students = $studentModel->findAll();
```

### Configuration

[](#configuration)

```
// app/Config/Tenantable.php
public $prefixFormat = 'tenant_{id}_{table}'; // Default format
public $baseDomain = 'example.com'; // Your production domain (or 'myapp.test' for local dev)
```

---

Strategy 2: Shared Database with tenant\_id
-------------------------------------------

[](#strategy-2-shared-database-with-tenant_id)

**Best for**: Simple applications, few tenants.

### Setup

[](#setup-1)

1. **Add tenant\_id to tables**

```
php spark make:migration add_tenant_id
```

2. **Use the Trait**

```
use nuelcyoung\tenantable\Traits\TenantableTrait;

class StudentModel extends Model
{
    use TenantableTrait;
    protected $table = 'students';
}
```

### Warning: Leakage Risks

[](#warning-leakage-risks)

Using `tenant_id` has security concerns:

- Forgetting to add trait to a model
- Raw SQL queries bypassing trait
- Joins missing tenant\_id
- IDOR attacks

**Use TenantTablePrefixTrait instead to eliminate these risks.**

---

Strategy 3: Separate Database Per Tenant
----------------------------------------

[](#strategy-3-separate-database-per-tenant)

**Best for**: Enterprise, strict compliance needs.

### Configuration

[](#configuration-1)

```
// app/Config/Tenantable.php
public bool   $separateDatabasePerTenant = true;
public ?string $isolationMode            = 'database';

// Point to your tenant-specific migrations
public ?string $tenantMigrationsNamespace = 'App\Database\Migrations\Tenant';

// Auto-provisioning (both true by default)
public bool $autoCreateDatabase = true;   // CREATE DATABASE on tenant insert
public bool $autoMigrateTenant  = true;   // Run migrations after creation

// Optional: custom database naming convention (default: tenant_{id})
public $databaseNameGenerator = null;

// Include third-party package migrations per-tenant (e.g. Shield)
public array $tenantMigrationsNamespaces = [
    'CodeIgniter\Shield',    // Shield's auth tables in every tenant DB
];
```

### How It Works

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

The database name is **derived dynamically** — it is never stored in the tenants table. By default the convention is `tenant_{id}` (e.g. `tenant_1`, `tenant_5`).

When you create a tenant:

```
php spark tenants:create acme "Acme Corp"
```

Or programmatically:

```
$tenantModel->insert(['name' => 'Acme Corp', 'subdomain' => 'acme']);
```

The package automatically:

1. Inserts the row into the `tenants` table
2. Creates the database: `CREATE DATABASE IF NOT EXISTS tenant_1`
3. Runs your tenant migrations (from `$tenantMigrationsNamespace`)
4. Runs third-party migrations (from `$tenantMigrationsNamespaces` — e.g. Shield)
5. Fires the `tenantCreated` event

On each request, the filter identifies the tenant and swaps `Config\Database::$default` to point at the tenant's database. All models transparently query the correct DB.

### DB Credentials

[](#db-credentials)

Connection credentials (host, user, password, port) come from your `.env` / `Config\Database::$default`. Only the database name changes per tenant. Your DB user must have `CREATE` privileges.

Credentials are **never stored in the tenants table** — storing secrets inside the database they unlock is a security foot-gun.

### Custom Naming

[](#custom-naming)

```
// Default: tenant_1, tenant_2, ...
public $databaseNameGenerator = null;

// Custom: myapp_acme, myapp_globex, ...
public $databaseNameGenerator = fn(array $tenant) => 'myapp_' . $tenant['subdomain'];
```

### Usage

[](#usage)

```
// Automatically switches to tenant's database
$school = tenant(); // Connects to tenant_1
$students = $studentModel->findAll(); // Queries tenant_1.students
```

---

Usage Examples
--------------

[](#usage-examples)

### Helper Functions

[](#helper-functions)

```
// Get current tenant ID
$tenantId = tenant_id();

// Get tenant data
$tenant = tenant();

// Check if tenant context exists
if (has_tenant()) {
    // Safe to query
}

// Generate tenant URL
$url = tenant_url('dashboard');

// Check if admin can bypass
if (can_bypass_tenant()) {
    // Access all tenants
}
```

### Manual Tenant Setting

[](#manual-tenant-setting)

```
use nuelcyoung\tenantable\Services\TenantManager;
use nuelcyoung\tenantable\Services\TenantTableManager;

TenantManager::getInstance()->setTenantById(1);
TenantTableManager::getInstance()->setTenant(1, 'school1');
```

### Bypassing (Superadmin)

[](#bypassing-superadmin)

```
// Temporarily bypass for specific query
Model::withoutTenant(function() {
    return Model::findAll(); // All tenants
});

// Or
Model::enableTenantBypass();
// queries...
Model::disableTenantBypass();
```

---

CLI Commands
------------

[](#cli-commands)

CommandPurpose`tenants:setup`Provision the central tenants table`tenants:create  `Create a tenant (auto-provisions DB in database mode)`tenants:list`List tenants (`--active`, `--inactive`)`tenants:run `Run a Spark command per tenant (auto-targets tenant DB in database mode)`tenants:make-model `Scaffold a tenant or global model`tenants:make-migration `Scaffold a tenant migration file### Examples

[](#examples)

```
# Initial setup
php spark tenants:setup

# Create tenants
php spark tenants:create foodblog "Food Blog"
php spark tenants:create acme "Acme Corp" --domain=acme.com

# Scaffold migrations
php spark tenants:make-migration CreatePostsTable --table=posts
php spark tenants:make-migration CreateCategoriesTable

# Scaffold models
php spark tenants:make-model Post
php spark tenants:make-model Post --prefix --table=posts

# Run migrations on all tenant databases (database mode)
php spark tenants:run migrate

# Rollback specific tenants
php spark tenants:run migrate:rollback --tenants=1,3
```

---

Package Structure
-----------------

[](#package-structure)

```
src/
├── Config/
│   └── Tenantable.php
├── Database/
│   └── Migrations/
│       └── CreateTenantsTable.php
├── Exceptions/
│   ├── TenantInactiveException.php
│   └── TenantNotFoundException.php
├── Filters/
│   ├── BaseTenantFilter.php
│   ├── SubdomainFilter.php
│   ├── DomainFilter.php
│   ├── DomainOrSubdomainFilter.php
│   ├── PathFilter.php
│   ├── RequestDataFilter.php
│   └── TenantFilter.php
├── Helpers/
│   └── tenantable_helper.php
├── Middleware/
│   └── TenantSecurityMiddleware.php
├── Models/
│   ├── GlobalModel.php
│   ├── TenantModel.php
│   └── TenantableModel.php
├── Services/
│   ├── TenantManager.php
│   ├── TenantDatabaseManager.php
│   └── TenantTableManager.php
└── Traits/
    ├── TenantTablePrefixTrait.php
    └── TenantableTrait.php

```

---

Database Schema
---------------

[](#database-schema)

The `tenants` table:

FieldTypeDescriptionidINTPrimary key (auto-increment)subdomainVARCHAR(50)Unique subdomain for SubdomainFilterdomainVARCHAR(255)Custom domain for DomainFilternameVARCHAR(255)Display nameis\_activeBOOLEANTenant statussettingsJSONCustom key-value settingscreated\_atDATETIMECreated timestampupdated\_atDATETIMEUpdated timestamp> **Note:** The database name is **not stored** in the table. It is derived at runtime via `Config\Tenantable::$databaseNameGenerator` (default: `tenant_{id}`).

---

Migration for Existing Apps
---------------------------

[](#migration-for-existing-apps)

### Option A: Table Prefix (Recommended)

[](#option-a-table-prefix-recommended)

1. Create tenants in `tenants` table
2. Create new prefixed tables for each tenant:
    - `tenant_1_students` (copy of students)
    - `tenant_2_students`
3. Delete old shared tables
4. Update models to use `TenantTablePrefixModel`

### Option B: Add tenant\_id

[](#option-b-add-tenant_id)

1. Add `tenant_id` column to all tables
2. Backfill with correct tenant IDs
3. Use `TenantableTrait` in models

---

Security Features
-----------------

[](#security-features)

- **TenantContext Middleware** - Enforces tenant on all requests
- **IDOR Protection** - Validates tenant\_id in requests
- **Global Table Protection** - Mark tables as exempt from prefixing
- **Audit Logging** - Log bypass attempts

---

Local Development
-----------------

[](#local-development)

When developing locally with **Laravel Herd**, **Valet**, or similar tools that serve sites under `.test` / `.local` TLDs, subdomain-based tenancy works out of the box — just set `baseDomain` to your local domain:

```
// app/Config/Tenantable.php
public string $baseDomain = 'myapp.test';
```

Or via environment variable:

```
TENANT_BASE_DOMAIN = myapp.test
```

Then access tenants at `acme.myapp.test`, `demo.myapp.test`, etc.

### DNS for Subdomains

[](#dns-for-subdomains)

PlatformWildcard DNSSetup**macOS** (Herd/Valet)✅ Built-inNo extra setup needed**Windows** (Herd)❌ Not built-inAdd entries to `hosts` file, or install [Acrylic DNS Proxy](https://mayakron.altervista.org/support/acrylic/Home.htm) for `*.myapp.test` wildcard**Linux**❌ Not built-inUse `dnsmasq` with `address=/.myapp.test/127.0.0.1`See [SETUP.md — Local Development](SETUP.md#12-local-development) for detailed instructions.

---

License
-------

[](#license)

MIT

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance90

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~11 days

Recently: every ~1 days

Total

8

Last Release

48d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/a7e410e4b7091b8001332a4f26e64534712ab330a1a684e34a71ed2a0000913e?d=identicon)[nuelcyoung](/maintainers/nuelcyoung)

---

Top Contributors

[![nuelcyoung](https://avatars.githubusercontent.com/u/33932707?v=4)](https://github.com/nuelcyoung "nuelcyoung (18 commits)")

---

Tags

subdomainsaastenantmulti-tenancycodeigniter4multitenantcodeignter4 multi tenantCodeIgniter4 Multi TenantCodeIgniter4 Multi-Tenant Systemcodeigniter4 tenancyCodeIgniter4 Multi-Tenant Framework

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/nuelcyoung-tenantable/health.svg)

```
[![Health](https://phpackages.com/badges/nuelcyoung-tenantable/health.svg)](https://phpackages.com/packages/nuelcyoung-tenantable)
```

###  Alternatives

[nunomazer/laravel-samehouse

A multi-tenant Laravel package, based on single database, simple and ease to use

24254.7k](/packages/nunomazer-laravel-samehouse)[abydahana/aksara

Aksara is a CodeIgniter based CRUD Toolkit you can use to build complex applications become shorter, secure and more reliable just in a few lines of code. Serving both CMS or Framework, produce both HEADLESS (RESTful API) or TRADITIONAL (Browser Based), just by writing single controller. Yet it's reusable, scalable and ready to use!

1111.2k](/packages/abydahana-aksara)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
