Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Postgres Schema Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations: this is a Symfony bundle, not a Laravel package. macpaw/postgres-schema-bundle ships a Bundle class and Symfony DI configuration, so composer require macpaw/postgres-schema-bundle will install the code but nothing will boot in a Laravel app — there is no service provider, no facade, no config/postgres-schema.php. For a Laravel developer the realistic value is reference implementation and concept, not drop-in dependency. With 0 stars and 0 dependents, treat it as a readable example of schema-per-tenant Postgres, not as a package you build a product on.

The v2.0.8 patch (fix: reset schema on transaction rollback, PR #23) is the most informative thing to happen to this package in a while: it confirms that the exact failure mode you'd worry about — a leaked search_path surviving a rolled-back transaction — was live in every release up to and including v2.0.7. Read that PR's diff before anything else; it is a short, concrete illustration of the one bug that matters in this problem space, and it tells you what the bundle's own authors consider the correct reset semantics.

Minimal steps:

  1. Read composer.json first — it tells you the PHP constraint, whether it pulls Doctrine DBAL/ORM, Symfony version bounds, and therefore how much of it is portable. If it depends on doctrine/dbal only, the core logic is likely just SQL + connection handling and is easy to lift.
  2. Read the bundle's DependencyInjection/Configuration.php (or equivalent) to see the actual config surface — connection names, schema naming strategy, default schema. That file is the best summary of what the authors intended.
  3. Read PR #23 and the v2.0.7...v2.0.8 diff. Then check the CHANGELOG/tags — last release is listed as 2026-07-06, which is suspicious against a 0-star repo; verify the tag actually exists and inspect the diff before trusting it. If you vendor anything, vendor v2.0.8 or later, never v2.0.7.

First use case to evaluate: schema-per-tenant Postgres (one CREATE SCHEMA tenant_42 per customer, shared public for reference data). If you need that, read their schema-creation and search_path code — plus the new rollback-reset path — then reimplement in a Laravel middleware + a custom connection config. That's an afternoon of work and gives you ownership.

Implementation Patterns

Connection-scoped search_path. The pattern that matters most. In Laravel, don't mutate config('database.connections.pgsql') at runtime for anything long-lived — build a separate connection entry:

config(['database.connections.tenant' => array_merge(
    config('database.connections.pgsql'),
    ['search_path' => $schema, 'schema' => $schema],
)]);
DB::purge('tenant');

Then use DB::connection('tenant') explicitly. Explicit connection names beat global state; you avoid the class of bug where a queued job runs against whichever schema the last request happened to leave behind.

Transaction-local SET LOCAL, plus an explicit reset on the failure path. For request-scoped work, prefer:

DB::transaction(function () use ($schema) {
    DB::statement("SET LOCAL search_path TO \"{$schema}\", public");
    // ... queries
});

SET LOCAL reverts on commit/rollback, so it cannot leak into a reused session. This is the single most important habit if you're on PgBouncer in transaction pooling mode. The v2.0.8 fix is precisely about the rollback half of that guarantee — which is a useful reminder that "the database reverts it for me" is only true if the reset is actually scoped correctly. If you also issue session-level SET search_path anywhere (provisioning, long-running workers, migrations), pair it with an explicit reset in a finally / catch block rather than trusting the rollback alone:

try {
    DB::statement("SET search_path TO \"{$schema}\", public");
    // ...
} finally {
    DB::statement('RESET search_path');
}

Migration repository per schema. Laravel's migrator writes to a single migrations table. For multi-schema you either (a) give each schema its own repository by overriding $connection->getMigrationTable(), or (b) run migrations once and only vary data schemas. Copy whichever approach the bundle takes — look for how it handles DDL ordering and whether it wraps schema creation in a transaction (Postgres DDL is transactional, so a failed migration can cleanly roll back the CREATE SCHEMA).

Schema lifecycle as Artisan commands. Wrap CREATE SCHEMA IF NOT EXISTS / DROP SCHEMA ... CASCADE in dedicated commands with a --dry-run. Provisioning needs to be idempotent and auditable; ad-hoc SQL in Tinker is how tenants get half-created.

Keep shared data in public. Reference tables (countries, plans, permissions) stay in public and get resolved by search_path fallback. This is the main reason schema-per-tenant scales better than database-per-tenant: cross-schema joins still work, and one connection pool serves everyone.

Gotchas and Tips

  • search_path is session state — and the rollback path is where packages get it wrong. Under persistent connections or a transaction-mode pooler, a set-but-not-reset search_path will serve tenant A's data to tenant B. Use SET LOCAL inside a transaction, or DISCARD ALL on release. This is the one bug that turns a multi-tenant app into an incident report, and this package shipped it until v2.0.8: v2.0.7 and earlier did not reset the schema when a transaction rolled back. Pin ^2.0.8 if you take the dependency at all, and if you port the code, port the fix too.
  • PG 15+ removed the public-schema default grant. CREATE on public is no longer granted to PUBLIC. Your migration role needs explicit GRANT CREATE ON SCHEMA public or schema creation will fail in a way that looks like a permissions mystery.
  • Schema owner ≠ table owner. If provisioning runs as one role and migrations as another, DROP SCHEMA will fail or orphan objects. Standardise on one migration role and use SET ROLE deliberately.
  • Symfony wiring is dead weight in Laravel. Don't try to register the bundle. If you genuinely want the code, fork it, delete the DependencyInjection/ and Resources/config/ folders, and vendor the remaining SQL/DBAL helpers.
  • Zero dependents means zero field reports. The v2.0.8 patch is a one-line-in-spirit rollback fix that took a dedicated PR against a package with no users — treat that as a signal about how much the happy paths have been exercised. Pin to a commit SHA or a hard ^2.0.8 floor, and write your own integration test that provisions a schema, migrates it, writes, reads, then deliberately throws inside a transaction and asserts the next query lands back in public. That rollback assertion is the test the package itself was missing until now.
  • Extension point to check for: a configurable schema naming strategy (tenant ID → schema name). If the bundle hardcodes a pattern, that's the first thing to make injectable in your port, because naming conventions always change (soft-deleted tenants, renames, ID vs. slug).
  • Debugging tip: when a query hits the wrong schema, SELECT current_schema(), current_setting('search_path') in the same connection before the failing query. Nine times out of ten the answer is "the connection was reused and nobody reset it" — and if the previous request rolled back, pre-2.0.8 code guaranteed exactly that.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route