laymont / postgres-multi-tenant
Laravel package for Postgres schema-per-tenant multi-tenancy
Requires
- php: ^8.2
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0
- phpunit/phpunit: ^11.0
README
Este paquete proporciona una base para implementar multi-tenancy schema-per-tenant en PostgreSQL para proyectos Laravel 12/13, con foco en buenas prácticas, tipado estricto y testabilidad.
Objetivo
- Cambiar dinámicamente el
search_pathde PostgreSQL por request/job/command. - Proveer una capa de resolución del tenant (domain/subdomain/header/path/etc.) sin acoplarlo a una app específica.
- Mantener el código del consumidor lo más limpio posible (SRP/DIP), exponiendo contratos claros.
Enfoque
Este paquete implementa el patrón probado en producción para Postgres schema-per-tenant:
- TenantContext: estado del schema actual.
- TenantSchemaManager: aplica
search_path(config + SQL) y permite volver apublic. - Listeners:
ConnectionEstablished: re-aplicasearch_pathal reconectar.TransactionBeginning: aplicaSET LOCAL search_pathal iniciar transacción.
- Guardrail opcional: validación de
current_schema()vs schema esperado.
Garantías anti-fuga (lo más importante)
- El
search_pathse aplica en conexión (ConnectionEstablished) y se re-sincroniza si se pierde. - En cada transacción se aplica
SET LOCAL search_pathpara reforzar aislamiento. - El middleware puede ejecutar un guardrail que compara
current_schema()con el schema esperado. - En entornos con workers persistentes (ej: Octane), el middleware hace reset a
publicenterminate(). - Opcionalmente, antes de cada query se re-aplica el
search_pathesperado (auto-healing) para proteger contra modificaciones fuera de banda.
Config:
apply_search_path_before_query:true(default)
Resolución de Tenant (baja fricción)
Por defecto, el paquete resuelve el schema desde el header:
X-Tenant-Schema: tenant_acme
Puedes cambiarlo definiendo postgres-multi-tenant.tenant_resolver con una clase que implemente Laymont\PostgresMultiTenant\Contracts\TenantResolver.
Resolvers incluidos:
Laymont\PostgresMultiTenant\Resolvers\HeaderTenantResolverLaymont\PostgresMultiTenant\Resolvers\AuthUserTenantResolver
Middleware recomendado
Middleware incluido:
Laymont\PostgresMultiTenant\Http\Middleware\SwitchTenantSchema
Config clave:
unresolved_tenant_mode:landlord(default) odenyreset_to_landlord_on_terminate:true(default)
Jobs (Laravel Queue)
Este paquete se apega al marco de Laravel:
- La persistencia de la cola (ej: tabla
jobs) debe vivir en el schema global (public). - La ejecución del job se hace en el schema del tenant (cambiando
search_path) y luego se resetea apublic.
Para eso se incluye un Job Middleware compatible con Laravel:
Laymont\PostgresMultiTenant\Queue\Middleware\RunJobInTenantSchema
Y un contrato para jobs tenant-aware:
Laymont\PostgresMultiTenant\Contracts\TenantAwareJob(métodotenantSchema(): ?string)
Migraciones (Laravel Migrations)
Este paquete también se apega al marco de Laravel:
- Laravel controla el catálogo global con su tabla
migrationsenpublic. - Para cada tenant, al ejecutar migraciones con
search_pathapuntando al schema del tenant, Laravel crea/usa una tablamigrationsdentro del schema del tenant.
Comando incluido:
php artisan tenant:migrate {schema}
Compatibilidad
- PHP:
^8.2 - Laravel:
^12.0|^13.0
Instalación
composer require laymont/postgres-multi-tenant
Implementación (proyecto nuevo)
1) Publica la configuración
php artisan vendor:publish --tag="laymont-postgres-multi-tenant-config"
2) Configura el comportamiento del paquete
Edita config/postgres-multi-tenant.php.
Recomendado (anti-fuga por defecto):
connection:pgsqlsearch_path_schemas:['{tenant}', 'public']enforce_schema_guardrail:truereset_to_landlord_on_terminate:trueapply_search_path_before_query:true
Nota: el paquete prioriza config() (cache-safe). En producción debes usar php artisan config:cache como en cualquier app Laravel.
3) Decide cómo resolver el tenant
Opción recomendada (API):
- Header
X-Tenant-Schema: tenant_acme
Si necesitas otra estrategia, define tenant_resolver con una clase que implemente:
Laymont\PostgresMultiTenant\Contracts\TenantResolver
4) Registra el middleware
El middleware incluido es:
Laymont\PostgresMultiTenant\Http\Middleware\SwitchTenantSchema
Ejemplo (Laravel 12/13): agrega el middleware donde tu aplicación registre middleware (por ejemplo, a nivel de rutas o grupo de rutas).
Recomendación:
- Aplica este middleware solo a las rutas tenant-aware (no necesariamente a todo el proyecto).
5) Define el contrato de “schema name”
Recomendación: usa un patrón de schemas consistente (por ejemplo tenant_acme) y mantén el regex en:
schema_pattern
Esto es una protección contra esquemas inválidos y reduce vectores de fuga.
Implementación (proyecto existente)
1) Asegura tu conexión pgsql
Verifica que tu config/database.php (o la configuración equivalente) use PostgreSQL y que la conexión que vaya a operar en modo tenant sea la misma que definiste en:
postgres-multi-tenant.connection
2) Evita fugas por workers persistentes
Si usas Octane o workers persistentes:
- Mantén
reset_to_landlord_on_terminate=true - Mantén
apply_search_path_before_query=true
3) Adopta gradualmente
Puedes empezar aplicando el middleware solo a un subset de endpoints tenant-aware.
Jobs (Laravel Queue) - implementación
Principio: cola global (tabla jobs en public) + ejecución tenant-aware.
Para que un job sea tenant-aware, implementa:
Laymont\PostgresMultiTenant\Contracts\TenantAwareJob
Y agrega el middleware al job:
Laymont\PostgresMultiTenant\Queue\Middleware\RunJobInTenantSchema
El middleware asegura:
- switch a tenant
- guardrail
- reset a landlord en
finally
Migraciones - implementación
Para migrar un schema tenant:
php artisan tenant:migrate tenant_acme
Esto ejecuta migrate de Laravel dentro del contexto del tenant, permitiendo que Laravel cree y use:
tenant_acme.migrations(tabla por tenant)
Verificación rápida (debug)
En Postgres puedes verificar el schema actual con:
select current_schema();
En runtime, el guardrail del paquete compara current_schema() contra el schema esperado cuando está habilitado.
Configuración
Publicar configuración:
php artisan vendor:publish --tag="laymont-postgres-multi-tenant-config"
Esto creará config/postgres-multi-tenant.php.
Uso
Ver secciones:
- Implementación (proyecto nuevo)
- Implementación (proyecto existente)
- Jobs (Laravel Queue)
- Migraciones (Laravel Migrations)
Licencia
MIT.