惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 司徒正美
宝玉的分享
宝玉的分享
美团技术团队
量子位
The Cloudflare Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
IT之家
IT之家
爱范儿
爱范儿
J
Java Code Geeks
博客园 - Franky
Last Week in AI
Last Week in AI
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
I
InfoQ
GbyAI
GbyAI
Recent Announcements
Recent Announcements
小众软件
小众软件
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
MyScale Blog
MyScale Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Multi-Tenant SaaS with Laravel: Automatic Data Isolation ...
Francesco Ca · 2026-05-12 · via DEV Community

Building a B2B SaaS platform for transport companies, I faced a critical architectural decision: separate database per tenant or shared database with logical isolation?

I chose shared database. Here's why and how I implemented bulletproof data isolation with pure Laravel.

Why Shared Database

For a B2B SaaS, database evolution is critical:

Pros:

  • Atomic migrations (add a column once, not 500 times)
  • Simplified backup/restore
  • Lower infrastructure costs

Cons:

  • Logical isolation (not physical)
  • Noisy neighbor risk

If I ever reach 500 tenants, managing 500 separate migrations per feature would be a full-time job. Shared DB wins.

The Architecture

USER REQUEST
     │
     ▼
MIDDLEWARE (EnsureTenantAccess)
     │ 1. Verify authentication
     │ 2. Extract tenant ID
     │ 3. Store in TenantContext (singleton)
     ▼
ELOQUENT MODELS (with BelongsToTenant trait)
     │ 4. Apply GlobalScope automatically
     ▼
DATABASE QUERY
     SELECT * FROM table WHERE tenant_id = [X]

Enter fullscreen mode Exit fullscreen mode

TenantContext: The Source of Truth

class TenantContext
{
    private ?string $tenantId = null;

    public function set(string $id): void
    {
        $this->tenantId = $id;
    }

    public function id(): string
    {
        return $this->tenantId;
    }

    public function isSet(): bool
    {
        return $this->tenantId !== null;
    }
}

Enter fullscreen mode Exit fullscreen mode

Singleton. One instance per request. Zero ambiguity.

BelongsToTenant Trait

This is where the magic happens:

trait BelongsToTenant
{
    public static function bootBelongsToTenant(): void
    {
        // READ: Auto-filter all queries
        static::addGlobalScope('tenant', function (Builder $query) {
            $context = app(TenantContext::class);
            if ($context->isSet()) {
                $query->where('tenant_id', $context->id());
            }
        });

        // WRITE: Auto-assign tenant_id on create
        static::creating(function (Model $model) {
            $context = app(TenantContext::class);
            if ($context->isSet() && !$model->tenant_id) {
                $model->tenant_id = $context->id();
            }
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

Every tenant-specific model (Customers, Vehicles, Transports) uses this trait. No manual tenant_id assignment. No forgetfulness bugs.

Testing Cross-Tenant Isolation

public function test_cross_tenant_isolation(): void
{
    $tenantA = Tenant::factory()->create();
    $tenantB = Tenant::factory()->create();

    Product::factory()->for($tenantA)->create(['name' => 'Item A']);
    Product::factory()->for($tenantB)->create(['name' => 'Item B']);

    $response = $this->actingAs($this->userInTenant($tenantA))
        ->getJson('/api/v1/products');

    $response->assertOk();
    $response->assertJsonCount(1, 'data');
    $response->assertJsonPath('data.0.name', 'Item A');
}

Enter fullscreen mode Exit fullscreen mode

Without this test, I'm just hoping the GlobalScope works. Hope is not a strategy.

Super Admin Impersonation

Support needs to see what customers see. Solution:

$tenantId = $user->isSuperAdmin()
    ? session('impersonate_tenant_id')
    : $user->tenant_id;

Enter fullscreen mode Exit fullscreen mode

Super Admin has no fixed tenant_id. Via dashboard, they select a tenant to impersonate. Session stores the ID. Middleware populates TenantContext. Same exact views as the customer.

Read-Only Mode

if ($tenant->is_read_only && $request->isMethodSafe() === false) {
    abort(403, 'Account in read-only mode');
}

Enter fullscreen mode Exit fullscreen mode

Use cases:

  • Payment suspension
  • Maintenance windows
  • Investigation locks

All centralized in middleware. Zero controller pollution.

Anti-Patterns Learned

  1. Manual tenant_id assignment: You WILL forget. Use the trait.
  2. Unique indexes without tenant_id: Always composite (tenant_id + unique_field)
  3. Incremental IDs for tenants: Use UUIDs. Prevents ID guessing attacks.

Would I Use a Package?

Spatie has spatie/laravel-multitenancy. It's solid.

But building my own gave me:

  • Full control over edge cases
  • Super admin impersonation (not trivial with packages)
  • Deep understanding of the isolation boundaries

If you're learning or need custom flows, build it yourself. If you need speed and standard patterns, use a package.

Conclusion

Multi-tenancy with Laravel is about trust: trust in your automation, distrust in human memory.

Global Scopes + TenantContext + comprehensive tests = sleep well at night.