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.

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:
- Subdomain —
acme.yourapp.comresolves to the Acme tenant - Custom domain —
app.acmecorp.comresolves via DNS lookup - Header —
X-Tenant-IDfor 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:
- Tenant selects a plan → Stripe subscription is created
- Payment fails → dunning flow with grace period and owner notification
- Plan changes → webhook-driven state sync keeps your DB accurate
- 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 operationsApp\Services\TenantService— tenant provisioning and managementApp\Services\InvitationService— team invite flowsApp\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.