PHPackages                             mahdisphp/laravel-realm - 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. mahdisphp/laravel-realm

ActiveLibrary

mahdisphp/laravel-realm
=======================

Multi-tenancy for Laravel. Safe by default.

v0.1.0(1mo ago)01↑2900%MITPHPPHP ^8.2

Since Mar 25Pushed 1mo agoCompare

[ Source](https://github.com/mahdi-salmanzade/laravel-realm)[ Packagist](https://packagist.org/packages/mahdisphp/laravel-realm)[ RSS](/packages/mahdisphp-laravel-realm/feed)WikiDiscussions main Synced 1mo ago

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

 [![Laravel Realm](realm-banner-custom.svg)](realm-banner-custom.svg)

 [![Version](https://camo.githubusercontent.com/74a6eb5040b6680759e86c164c9a487587d7ad15642b7bf30a7cd9d4ee899892/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d61686469737068702f6c61726176656c2d7265616c6d)](https://packagist.org/packages/mahdisphp/laravel-realm) [![Downloads](https://camo.githubusercontent.com/8007f26a5930fae5df2ac4e962e08a25b0d0d77451d764cf25ff5db62c4490d4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d61686469737068702f6c61726176656c2d7265616c6d)](https://packagist.org/packages/mahdisphp/laravel-realm) [![License](https://camo.githubusercontent.com/926a63fc79b2a186f580cd1c292a0bfd9c05bfa7e291348af8cb343fab80e8c6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6d61686469737068702f6c61726176656c2d7265616c6d)](LICENSE)

---

What is Realm?
--------------

[](#what-is-realm)

Realm adds multi-tenancy to your Laravel app with a single trait and a safety-first approach. It's designed for the 80% of SaaS apps that need row-level tenant isolation in a single database.

**Column strategy only.** Database-per-tenant and schema-per-tenant strategies are coming in v1.0.

### What makes Realm different?

[](#what-makes-realm-different)

- **Fail-closed by default.** If no tenant context is active, queries on tenant models return empty results — not all tenants' data. Data leaks should be impossible by default.
- **Clean naming.** The `Realm` facade and `Tenant` model are separate classes with zero method overlap. No ambiguity.
- **One trait, one macro.** Add `BelongsToRealm` to a model and `$table->realm()` to a migration. That's it.
- **Octane-ready.** Context is automatically reset between requests. No manual setup required.

### What Realm is NOT (yet)

[](#what-realm-is-not-yet)

- Battle-tested. This is a new package. Use in production at your own risk and report bugs.
- A replacement for [stancl/tenancy](https://tenancyforlaravel.com/). Stancl has years of production edge cases discovered and fixed. If you need database-per-tenant today, use stancl.

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

[](#requirements)

- PHP 8.2+
- Laravel 11, 12, or 13

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

[](#installation)

```
composer require mahdisphp/laravel-realm
php artisan realm:install
```

Quick Start
-----------

[](#quick-start)

### 1. Add the trait to your model

[](#1-add-the-trait-to-your-model)

```
use Realm\Traits\BelongsToRealm;

class Project extends Model
{
    use BelongsToRealm;
}
```

### 2. Add the column to your migration

[](#2-add-the-column-to-your-migration)

```
Schema::create('projects', function (Blueprint $table) {
    $table->id();
    $table->realm();  // Adds realm_id + index
    $table->string('name');
    $table->timestamps();
});
```

### 3. Wrap your routes

[](#3-wrap-your-routes)

```
// Tenant routes — realm context is resolved automatically
Route::realm(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::resource('projects', ProjectController::class);
});

// Central routes — guaranteed no tenant context
Route::central(function () {
    Route::get('/', [MarketingController::class, 'index']);
});
```

### 4. That's it

[](#4-thats-it)

```
// Inside a tenant route, queries are scoped automatically
Project::all();           // Only current tenant's projects
Project::create([...]);   // realm_id set automatically

// Outside tenant context (strict mode) — returns empty, not everything
Project::all();           // [] — fail-closed, logged warning
```

Key Concepts
------------

[](#key-concepts)

### The Facade vs The Model

[](#the-facade-vs-the-model)

```
// FACADE — context management (Realm)
Realm::current();                    // Get active Tenant model
Realm::id();                         // Get active tenant ID
Realm::key();                        // Get active tenant key
Realm::check();                      // True if tenant set & tenancy enabled
Realm::isTenancyDisabled();          // True inside withoutTenancy()
Realm::run('acme', fn() => ...);     // Execute in a tenant's context
Realm::withoutTenancy(fn() => ...);  // Disable all scoping
Realm::forget();                     // Clear active tenant (keep tenancy state)

// FACADE — secrets
Realm::setSecret('acme', 'key', 'value');
Realm::getSecret('acme', 'key');
Realm::deleteSecret('acme', 'key');

// MODEL — data operations (Tenant)
Tenant::create(['key' => 'acme', 'name' => 'Acme Corp']);
Tenant::where('active', true)->get();
```

### Strict Mode (Default: ON)

[](#strict-mode-default-on)

When no tenant context is active:

- **Model queries** return empty results (`WHERE 0 = 1`) and log a warning
- **Model creates** throw `NoActiveRealmException`
- Cache and storage fall back to global (unprefixed) space

### Escape Hatches

[](#escape-hatches)

```
// Bypass query scope for ONE query (cache/storage still scoped)
Project::withoutRealm()->get();

// Bypass ALL scoping (queries + cache + storage)
Realm::withoutTenancy(fn() => Project::all());

// Query a specific tenant
$acme = Tenant::where('key', 'acme')->first();
Project::forRealm($acme)->get();
```

### Shared vs Tenant Models

[](#shared-vs-tenant-models)

Not every model needs tenant scoping:

```
// TENANT MODEL — scoped, has realm_id
class Project extends Model { use BelongsToRealm; }

// SHARED MODEL — no trait, no realm_id, available to all tenants
class Category extends Model { /* no BelongsToRealm */ }

// Relationships work across the boundary:
// $project->category works (Category is not scoped)
```

Cache / Queue / Storage Isolation
---------------------------------

[](#cache--queue--storage-isolation)

### Cache Key Prefixing

[](#cache-key-prefixing)

When a tenant is active, all cache keys are automatically prefixed with `realm:{tenant_key}:`. No code changes needed.

```
// Inside tenant 'acme' context:
Cache::put('settings', $data);  // Actually stores as realm:acme:settings
Cache::get('settings');          // Reads from realm:acme:settings

// Inside withoutTenancy — reads/writes global keys
Realm::withoutTenancy(fn() => Cache::get('settings'));  // Reads 'settings' (global)
```

### Queue Context Propagation

[](#queue-context-propagation)

Add the `RealmAwareJob` trait to your jobs. It captures the tenant context at dispatch time and restores it when the job is processed.

```
use Realm\Traits\RealmAwareJob;
use Realm\Integrations\RealmQueueMiddleware;

class GenerateReport implements ShouldQueue
{
    use RealmAwareJob;

    public function middleware(): array
    {
        return $this->realmMiddleware();
    }

    public function handle(): void
    {
        // Realm context is automatically restored here
    }
}
```

- In strict mode, dispatching without context throws `NoActiveRealmException`
- If the tenant is deleted between dispatch and processing, the job is failed
- If the tenant is deactivated, the job is released with a configurable delay

### Storage Path Prefixing

[](#storage-path-prefixing)

All storage paths are automatically prefixed with `tenants/{tenant_key}/`:

```
// Inside tenant 'acme' context:
Storage::put('logo.png', $file);  // Stores at tenants/acme/logo.png
Storage::get('logo.png');          // Reads from tenants/acme/logo.png
```

Encrypted Secrets
-----------------

[](#encrypted-secrets)

Store sensitive per-tenant configuration (API keys, tokens) encrypted at rest:

```
// On the Tenant model
$tenant->setSecret('stripe_key', 'sk_live_...');
$tenant->getSecret('stripe_key');   // Returns decrypted value
$tenant->deleteSecret('stripe_key');

// Via the Facade
Realm::setSecret('acme', 'stripe_key', 'sk_live_...');
Realm::getSecret('acme', 'stripe_key');
```

Secrets are:

- Encrypted via Laravel's `Crypt` facade
- Hidden from `toArray()` / `toJson()` (never accidentally serialized)
- Cascade-deleted when the tenant is deleted

Tenant User Relationships
-------------------------

[](#tenant-user-relationships)

The `tenant_users` pivot table links users to tenants with roles:

```
// Attach a user
$tenant->users()->attach($user, ['role' => 'owner']);

// Query users
$tenant->users;                              // All users for this tenant
$tenant->users()->wherePivot('role', 'owner')->first();
```

Per-Tenant Config Overrides
---------------------------

[](#per-tenant-config-overrides)

Store non-sensitive config overrides in the tenant's `data` JSON column:

```
$tenant = Tenant::create([
    'key' => 'acme',
    'name' => 'Acme Corp',
    'data' => [
        'config' => [
            'app.name' => 'Acme Dashboard',
            'mail.from.name' => 'Acme Support',
        ],
    ],
]);

// Inside Realm::run() or middleware, config() returns tenant-specific values
Realm::run($tenant, function () {
    config('app.name'); // 'Acme Dashboard'
});
// After run(), original config values are restored
```

> API keys and secrets should use `setSecret()`, not config overrides.

Resolvers
---------

[](#resolvers)

Realm resolves the tenant from the incoming request using a pipeline of resolvers. The first match wins.

ResolverConfigExample`SubdomainResolver``realm.subdomain.domain``acme.myapp.com``HeaderResolver``realm.header.name``X-Realm: acme``PathResolver``realm.path.segment``myapp.com/acme/dashboard``DomainResolver`—`app.acmecorp.com` (full custom domain)`QueryResolver``realm.query.parameter``?realm=acme``SessionResolver``realm.session.key`Session-based switching`AuthResolver`—Resolves from authenticated user's tenantConfigure active resolvers in `config/realm.php`:

```
'resolvers' => [
    SubdomainResolver::class,
    HeaderResolver::class,
    // Add more as needed
],
```

Commands
--------

[](#commands)

```
php artisan realm:create acme "Acme Corp"              # Create a tenant
php artisan realm:delete acme                           # Delete (with confirmation)
php artisan realm:delete acme --force                   # Delete without confirmation
php artisan realm:list                                  # List all tenants
php artisan realm:check                                 # Diagnostic health check
php artisan realm:check --realm=acme                    # Inspect specific tenant
php artisan realm:test                                  # Verify isolation works
php artisan realm:exec acme "cache:clear"               # Run command as tenant
php artisan realm:run-for-each "cache:clear"            # Run command for all tenants
php artisan realm:run-for-each "reports:generate" --only=acme --only=globex
```

Testing
-------

[](#testing)

```
use Realm\Testing\RealmTestHelpers;

class ProjectTest extends TestCase
{
    use RealmTestHelpers;

    public function test_projects_are_isolated(): void
    {
        $acme = $this->createRealm('acme');
        $globex = $this->createRealm('globex');

        $this->actingAsRealm($acme, fn() => Project::create(['name' => 'Acme Project']));
        $this->actingAsRealm($globex, fn() => Project::create(['name' => 'Globex Project']));

        $this->assertRealmCount($acme, Project::class, 1);
        $this->assertRealmMissing($acme, Project::class, ['name' => 'Globex Project']);
    }
}
```

Blueprint Macro
---------------

[](#blueprint-macro)

```
$table->realm();                   // Default: restrictOnDelete (safe)
$table->realm(cascade: true);      // Opt-in: cascadeOnDelete
```

`restrictOnDelete` is the default — deleting a tenant fails with a foreign key error, forcing you to handle data cleanup explicitly.

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

[](#configuration)

After installation, the config lives at `config/realm.php`:

- `strategy` — `'column'` (only option currently)
- `resolvers` — array of resolver classes
- `strict` — `true` (fail-closed on missing context)
- `cache.prefix` — `true` (auto-prefix cache keys per tenant)
- `queue.context` — `true` (propagate tenant context to queued jobs)
- `storage.prefix_path` — `true` (auto-prefix storage paths per tenant)
- `user_model` — the user model class for tenant-user relationships
- `on_fail` — `'abort'` (abort/redirect/exception on resolution failure)

Why not stancl/tenancy?
-----------------------

[](#why-not-stancltenancy)

Use stancl if you need database-per-tenant today — it has years of battle-testing we don't. Realm is for the 80% of SaaS apps that only need column-based row isolation and want it with one trait, one macro, and fail-closed defaults out of the box.

Roadmap
-------

[](#roadmap)

- **v0.1** — Column strategy, subdomain + header resolution, strict mode, test helpers
- **v0.2** (current) — Cache/queue/storage isolation, encrypted secrets, tenant users, 5 new resolvers, per-tenant config overrides, `realm:delete`/`realm:exec`/`realm:run-for-each` commands
- **v1.0** — Database-per-tenant and schema-per-tenant strategies

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

35

—

LowBetter than 80% of packages

Maintenance90

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 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

Unknown

Total

1

Last Release

47d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/06083d1d7eb78d70ca4d7e348ca8dd66bc5b44be6d671a44a57b4bc778c44812?d=identicon)[mahdisphp](/maintainers/mahdisphp)

---

Top Contributors

[![mahdi-salmanzade](https://avatars.githubusercontent.com/u/23733945?v=4)](https://github.com/mahdi-salmanzade "mahdi-salmanzade (3 commits)")

---

Tags

laravellaravel-packagemulti-tenancymulti-tenantphprealmsaastenancytenantlaravelsaasmulti-tenancytenancyrealm

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/mahdisphp-laravel-realm/health.svg)

```
[![Health](https://phpackages.com/badges/mahdisphp-laravel-realm/health.svg)](https://phpackages.com/packages/mahdisphp-laravel-realm)
```

###  Alternatives

[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k25.9M107](/packages/laravel-cashier)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k43.5M5.2k](/packages/larastan-larastan)[laravel/passport

Laravel Passport provides OAuth2 server support to Laravel.

3.4k85.0M531](/packages/laravel-passport)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9682.1M97](/packages/roots-acorn)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k12.1M99](/packages/laravel-pulse)[spatie/laravel-enum

Laravel Enum support

3655.4M31](/packages/spatie-laravel-enum)

PHPackages © 2026

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