Engineering
3 min read

Laravel Multi-Tenant Architecture: Lessons from Building TenantX

Key architectural decisions we made building TenantX with Laravel 12 and PostgreSQL RLS, and why they matter for production multi-tenant SaaS applications.

Laravel Multi-Tenant Architecture: Lessons from Building TenantX

Building a multi-tenant SaaS application with Laravel involves hundreds of architectural decisions. After building TenantX, here are the ones that matter most.

Application-Layer Tenant Isolation Over Scattered tenant_id Filters

The most common approach to multi-tenancy is adding a tenant_id column to every table and filtering every query ad hoc. This approach is fragile:

  • One forgotten WHERE clause and you have a data breach
  • Every developer must remember to filter every query, every time
  • SQL injection can bypass application-level filtering

TenantX enforces tenant isolation at the application layer — centralized in middleware and service classes rather than scattered across individual queries. The resolved tenant context is set once per request and threaded consistently through every route, API, and data access layer.

// TenantMiddleware.php — resolved once, applied everywhere
app()->instance('current_tenant', $tenant);

This means there is one place to audit, one place to change, and a consistent contract across the codebase.

Tenant Resolution

TenantX resolves the current tenant once per request via middleware:

  • Subdomainacme.yourapp.com resolves to the Acme tenant
  • Custom domainapp.acmecorp.com resolves via DNS lookup
  • HeaderX-Tenant-ID for API requests

The resolved tenant ID is then set in the PostgreSQL session variable. From that point, every database query is automatically scoped.

Inertia.js for Seamless Laravel-React Integration

Instead of building a separate API and React SPA, TenantX uses Inertia.js to bridge Laravel and React:

  • No API layer to maintain — Laravel controllers return Inertia responses directly
  • Server-side routing — use Laravel's router, middleware, and form validation
  • TypeScript throughout — React components are fully typed
  • Server-side rendering — faster initial page loads

This gives you the developer experience of a modern React SPA with the productivity of Laravel's server-side patterns.

Subscription Billing with Stripe

A common mistake in SaaS billing is treating it as an afterthought. In TenantX, billing is wired in from the start:

  1. Tenant selects a plan → Stripe subscription is created
  2. Payment fails → dunning flow with grace period and owner notification
  3. Plan changes → webhook-driven state sync keeps your DB accurate
  4. All state changes are webhook-driven for reliability

Granular RBAC with Middleware and React Hooks

TenantX implements three workspace roles — Admin, Member, Contributor — with permissions enforced at every layer:

  • Laravel middleware gates route access
  • Policy classes control model-level operations
  • React hooks like usePermission('manage-members') for conditional UI rendering

This dual enforcement means permissions are checked server-side for security and client-side for UX.

Centralized External Integrations

All external API calls go through dedicated service classes:

  • App\Services\StripeService — all Stripe billing operations
  • App\Services\TenantService — tenant provisioning and management
  • App\Services\InvitationService — team invite flows
  • App\Services\NotificationService — all email and notification sending

This means you can swap providers, add retry logic, or modify behavior in one place without hunting through controllers.

The Takeaway

Good multi-tenant architecture is not about using the newest framework features. It is about making decisions that prevent data breaches, simplify billing, and reduce operational pain six months from now. Every pattern in TenantX exists because the alternative causes real problems in production.