mohamedmohamedhekal / tenantforge
Shared-database multi-tenancy foundation for Laravel SaaS: tenants, domains, memberships, API keys, and isolation scopes
Requires
- php: ^8.2
- illuminate/auth: ^11.0|^12.0
- illuminate/contracts: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/events: ^11.0|^12.0
- illuminate/hashing: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/phpstan: ^2.0
README
Search terms: laravel, multitenancy, saas, tenants, api-keys, teams, php, laravel-package, multi-tenant, b2b-saas, isolation.
Shared-database multi-tenancy foundation for Laravel SaaS: tenants, domains, memberships, hashed API keys, plan stubs, and audited impersonation.
This is plumbing—not a finished product. Use it to start a B2B app without reinventing isolation basics.
Installation
composer require mohamedhekal/tenantforge php artisan vendor:publish --tag=tenantforge-config php artisan migrate
Provision a tenant
use Hekal\TenantForge\Facades\TenantForge; $tenant = TenantForge::provision( name: 'Acme', ownerUserId: $user->id, slug: 'acme', domain: 'acme.app.test', planSlug: 'pro', );
Scope models
use Hekal\TenantForge\Concerns\BelongsToTenant; class Project extends Model { use BelongsToTenant; }
Initialize tenancy (middleware or manually):
Route::middleware('tenantforge.init')->group(...); // identification: header X-Tenant | domain | subdomain (config)
API keys
['plain' => $plain, 'key' => $key] = TenantForge::apiKeys()->create($tenant, 'CI'); // store $plain once; only the hash is persisted
Middleware alias: tenantforge.api (X-Api-Key).
Entitlements (stubs)
Plans store a features list/map. TenantForge::allows('webhooks') checks the tenant’s active subscription.
Wire FlagDeck later for richer targeting; v0.1 keeps plans local and explicit.
Impersonation
$log = TenantForge::impersonation()->start($tenant, $adminId, $userId, reason: 'ticket #12'); TenantForge::impersonation()->stop($log);
Limitations (v0.1)
- Shared DB only (no DB-per-tenant driver yet)
- No Stripe/Paddle billing
- No Vue portal or Filament admin bundled
- Without tenant context,
BelongsToTenantdoes not filter (intentional for migrations/CLI)—always initialize in HTTP/jobs
Testing
composer install && composer test
Isolation tests live under tests/Feature/TenancyTest.php.
License
MIT