macpaw/postgres-schema-bundle
Symfony bundle for PostgreSQL schema-based multi-tenancy. Switch the active database schema per request or tenant, integrate with Doctrine, and isolate data without managing separate databases. Includes schema-aware connection and listener support.
Architecture fit
The package is named macpaw/postgres-schema-bundle and packaged as a Symfony bundle, not a Laravel package. That distinction remains the single most important finding for a TPM sizing this work: Laravel has no bundle lifecycle. There is no Bundle class, no DI Extension, no compiler passes, and no bundle registration in config/app.php or bootstrap/providers.php. Whatever value exists in the repo has to be re-hosted inside a Laravel service provider before it can be evaluated at all. The v2.0.8 patch release does not change this.
The reusable surface is likely narrow but real: Postgres schema DDL logic, search_path management, and console commands (Laravel already runs on Symfony Console, so command classes are largely portable). The non-portable surface is the Symfony wiring, config loading (YAML/XML rather than Laravel's config/*.php array style), and any Doctrine DBAL coupling. Laravel's relationship to Doctrine is transactional only — doctrine/dbal is pulled in for specific schema-introspection paths, not as a data-access layer — so a bundle that assumes Doctrine connections, Doctrine migrations, or Doctrine event subscribers will need an adapter layer rather than a shim.
Net: poor packaging fit, moderate logic fit. Treat this as "a library of Postgres schema operations you may be able to extract," not "a package you install."
Integration feasibility
Feasible, but it is a build project, not an adoption project. A realistic path is: (1) stand the bundle up in a throwaway Symfony skeleton to learn the actual API surface and confirm what it does; (2) extract the framework-agnostic core; (3) write a Laravel provider, config file, and command wrappers; (4) wire search_path into Laravel's connection resolver and reset hooks. Estimate 2–4 engineer-weeks to production-ready, most of it in steps 3–4 and in testing the tenant-isolation boundary.
v2.0.8 is relevant to step 4 specifically: it fixes schema state not being reset on transaction rollback, which is one of the state-lifecycle edges that a Laravel port would otherwise have to discover and patch itself. That is a small but genuine de-risking of the port — it is evidence the maintainers are actively working the hard part (session-state correctness across lifecycle boundaries) rather than only packaging.
Before committing, this must be compared against build-vs-buy alternatives. The capability domain — Postgres schema-per-tenant management — is well served by stancl/tenancy and spatie/laravel-multitenancy, both of which are Laravel-native, have real adoption, and already solve the Octane/queue-worker search_path leak problem. A TPM should require an explicit written justification for choosing a zero-adoption third-party core over either a maintained package or ~2 weeks of in-house DDL code.
Technical risk
Still high, concentrated in adoption signals rather than code quality, but the picture has shifted slightly on maintainer intent:
@alekseytupichenkov), with a versioned tag and a compare link. That is a working PR-and-release workflow, not a dead drop. It answers key question #6 in part: there is a reachable human, and the repo is being maintained. It does not answer whether MacPaw is using it internally or whether external issues get triaged.search_path is session state. Any place where it can leak across requests — Octane workers, queued jobs, connection pools, and (per this release) rollback paths — is a cross-tenant data-exposure path. v2.0.8 closes one such path upstream; it does not close the ones the Laravel port would introduce.Key questions
search_path routing, or all three? The description ("Postgres schema bundle") does not say. Still open.search_path behavior? A regression that shipped in v2.0.7 and was fixed in v2.0.8 suggests lifecycle-edge coverage was incomplete at least once. Verify current coverage; specifically ask whether the rollback scenario now has a regression test.Stack fit
The underlying technology is a good fit for the target stack — Postgres 13+ with schema-per-tenant is a legitimate, well-understood multi-tenancy model, and Laravel runs on Symfony Console so command classes survive the port. The friction is entirely at the packaging and data-access layers: Symfony DI config, Doctrine assumptions, and YAML configuration do not map onto Laravel idioms. Assume roughly 20% of the codebase is reusable as-is, 50% is portable with an adapter, and 30% (Symfony wiring) is discarded.
Migration path
Sequence the work so the go/no-go decision lands before any Laravel-specific investment:
stancl/tenancy and a ~300-line in-house DDL implementation. Only proceed to step 3 if the bundle does something materially difficult that the alternatives do not.UPSTREAM.md, extract the domain layer, write PostgresSchemaServiceProvider, publishable config array, and command wrappers. Carry the v2.0.8 rollback fix through any refactor — it is a correctness fix, not cosmetic.search_path into ConnectionEstablished/ConnectionResolved and, critically, into Octane request lifecycle, queue worker job boundaries, and transaction rollback / exception unwind paths — the last being the class of bug v2.0.8 just fixed upstream. Assume the Laravel port will reintroduce that bug unless it is tested for.Compatibility
Pin and verify against a specific Laravel minor before the spike, not after. Validate three things in parallel: DBAL constraint resolution, PHP constraint resolution against the runtime image, and whether the bundle's config schema can express what the target environment needs (schema naming, tenant identifier validation, connection-per-tenant vs search_path switching). Treat the fork as an internal package with its own version line — do not depend on upstream tags for production stability, even though upstream tagging is now demonstrably functional.
Sequencing
The dependency chain is strict: spike → decision gate → adapter → isolation testing → staged rollout. Isolation testing (step 4) must be a hard gate, not a parallel track, because everything downstream assumes it passed. Budget the majority of engineering time there rather than in the porting work, which is comparatively mechanical. Add one explicit test this cycle: mid-transaction rollback with an active tenant, asserting search_path returns to the expected state — this is a direct regression test against the v2.0.7 defect and costs hours, not days.
Maintenance
You will own this permanently in practice, but the cost estimate improves marginally. v2.0.8 demonstrates an active, reviewable release process with attributable commits, which reduces — but does not eliminate — the "abandoned experiment" scenario. Zero dependents still means upstream will not triage your issues, and there is no changelog discipline beyond the auto-generated compare link. Budget ongoing fork maintenance — rebasing, DBAL constraint updates, Postgres major-version compatibility — as a standing line item, roughly a few days per quarter. The upside of an active upstream is that some correctness work (like this rollback fix) arrives for free if you stay close to upstream; the downside is that each upstream release is a rebase you must validate, since you have no contributor stake in it.
Support
MIT, no vendor, no SLA, no security advisory channel. Security patching is entirely internal, and there is no CVE feed to watch. Mitigate by keeping the fork small and well-tested, documenting the exact behavioral contract the application depends on, and assigning a named internal owner. The risk here is not that the code is bad — it is that nobody outside the team will ever tell you it is. A named upstream maintainer is a courtesy channel, not a support contract; do not plan around it.
Scaling
Schema-per-tenant scales well to hundreds of schemas and poorly to tens of thousands: Postgres catalog performance degrades as pg_catalog grows, autovacuum works per-table across all schemas, and migrations become fan-out operations that must be chunked and made resumable. Connection handling is the sharper edge — with PgBouncer in transaction mode, session-level search_path is not reliably scoped and the safe configuration is per-tenant credentials or per-tenant databases rather than SET search_path. Establish the expected tenant ceiling and connection-pool topology before the adapter work, because those choices change the design.
Failure modes
Rank by severity, not likelihood:
search_path leakage across requests or jobs → cross-tenant data exposure. Highest severity, highest likelihood in an Octane or queue-heavy deployment. Mitigate with explicit set-and-reset around every unit of work and an automated test that asserts isolation under concurrent load.search_path not reset on transaction rollback or exception unwind → subsequent statements in the same session execute against the wrong schema. Severity is equivalent to the above; the defect class was live upstream through v2.0.7 and is fixed in v2.0.8. This is now the highest-value thing to verify in the spike and the highest-value test to carry into the fork.SET → schema injection. Validate against a strict allowlist pattern before any identifier reaches SQL; identifiers cannot be parameterized.Ramp-up
Moderate. The porting engineer needs genuine fluency in both Symfony DI and Laravel's container and lifecycle hooks — an uncommon combination, and a poor fit for a generalist PHP developer. Expect two to three weeks before that person is productive on this specific task, plus the isolation-testing effort. On the positive side, the conceptual model (Postgres schemas as tenant boundaries) is simple and well documented elsewhere; almost none of the ramp-up cost is domain learning.
TPM recommendation
The score (14.4) with zero stars and zero dependents, packaged for a framework the target stack does not use, remains a do-not-adopt as a dependency signal. The opportunity score (46.9) is defensible — multi-tenant Postgres schema management is genuinely valuable — but the value lives in the capability, not in this artifact.
v2.0.8 does not change the verdict, but it improves two inputs and should be reflected in the decision record: there is a named, active maintainer with a functioning PR/release workflow, and one of the top-ranked failure modes (state reset on rollback) has been actively worked upstream. Recommended action: fund the 5-day spike against a pinned v2.0.8, and add two acceptance criteria to it — reproduce the rollback scenario and confirm the fix, and inspect whether a regression test accompanies it. If both hold, the "extract the core" path becomes more credible; if the spike shows the core is inseparable from Symfony/Doctrine wiring, fall back to stancl/tenancy or in-house DDL and cite this bundle as design reference only.
How can I help you explore Laravel packages today?