PostgresMultitenant

Schema-based multi-tenancy for Rails applications backed by PostgreSQL schemas.

PostgresMultitenant provides:

  • request middleware that switches PostgreSQL search_path by tenant subdomain
  • schema lifecycle helpers for creating, dropping, switching, and listing tenants
  • tenant schema sync from db/tenant_schema.rb, with db/schema.rb fallback
  • Rails rake tasks for tenant lifecycle operations
  • a Rails install generator

Requirements

  • Ruby 3.1+
  • Rails 6.1+
  • PostgreSQL with schema_search_path support

The gem is compatible with Rails 8.1.x, including Rails 8.1.3. Rails 8.1 itself requires Ruby 3.2+, so applications on Ruby 3.1 should stay on an older compatible Rails release.

Installation

Add the gem to your Rails app:

gem "postgres_multitenant"

Then run:

bundle install
bin/rails generate postgres_multitenant:install

The generator creates:

  • config/initializers/postgres_multitenant.rb
  • db/tenant_schema.rb

Generator options:

bin/rails generate postgres_multitenant:install --tenant-class-name=Account --tenant-foreign-key=account_id

Configuration

Edit config/initializers/postgres_multitenant.rb after running the generator:

PostgresMultitenant.configure do |config|
  # Public-schema model that identifies tenants by subdomain.
  config.tenant_class_name = "Organization"

  # Required: return tenant-scoped table names without schema prefixes.
  config.tenant_table_resolver = -> { %w[clients projects bookings] }

  # Optional: scope tenant records considered for routing and tenant migration.
  config.tenant_scope_proc = ->(klass) { klass.where(active: true) }

  # Optional: custom subdomain lookup.
  config.tenant_finder = ->(subdomain) { Organization.find_by(subdomain: subdomain) }

  # Optional: custom schema naming.
  config.tenant_schema_name_proc = ->(tenant) { tenant.subdomain.tr("-", "_") }

  # Tenant foreign key expected on tenant-scoped tables during development checks.
  config.tenant_foreign_key = "organization_id"

  # Optional: override request-to-subdomain extraction.
  # config.subdomain_extractor = ->(request) { request.subdomain.presence }

  # Strategy when tenant is not found:
  #   :raise  -> raise PostgresMultitenant::TenantNotFoundError
  #   :public -> continue in config.default_schema
  #   :custom -> call config.tenant_not_found_handler
  config.tenant_not_found_strategy = :raise

  # Required when tenant_not_found_strategy is :custom.
  # config.tenant_not_found_handler = ->(_request, _subdomain) {
  #   [404, {"Content-Type" => "text/plain"}, ["Tenant not found"]]
  # }
end

Data Model Contract

Your tenant class, for example Organization, should:

  • live in the public schema
  • expose a subdomain attribute, unless tenant_finder is configured
  • expose a schema name through schema_name, or use tenant_schema_name_proc

Tenant-scoped tables should include the configured tenant foreign key, such as organization_id, unless the table is listed in config.system_tables.

Tenant Schema

Prefer defining tenant-only tables in db/tenant_schema.rb:

ActiveRecord::Schema.define do
  create_table "projects", force: :cascade do |t|
    t.bigint "organization_id", null: false
    t.string "name", null: false
    t.timestamps
  end
end

If db/tenant_schema.rb is absent, PostgresMultitenant loads db/schema.rb and prunes tables that are not returned by:

  • config.tenant_table_resolver.call
  • config.system_tables

Usage

PostgresMultitenant.switch("acme") { Project.count }
PostgresMultitenant.create("acme")
PostgresMultitenant.drop("acme")
PostgresMultitenant.current #=> "public" or tenant schema
PostgresMultitenant.tenants #=> ["acme", "beacon"]
PostgresMultitenant.reset!

Schema names must be lowercase PostgreSQL-safe identifiers matching:

\A[a-z][a-z0-9_]*\z

Reserved schemas such as public, information_schema, and pg_* cannot be used as tenant schemas.

Rake Tasks

bin/rails tenants:create[acme]
bin/rails tenants:drop[acme]
bin/rails tenants:list
bin/rails tenants:migrate

tenants:migrate keeps tenant schemas in sync by loading db/tenant_schema.rb or db/schema.rb; it does not run Rails migrations inside tenant schemas.

Development

Run the test suite:

rake test

Build the gem:

gem build postgres_multitenant.gemspec

Security Notes

  • Keep tenant_not_found_strategy = :raise unless public-schema fallback is intentional.
  • Keep public tables and tenant tables separated by design.
  • Do not include tenant-owned data in public-schema models.
  • Review config.tenant_table_resolver whenever adding tenant-scoped tables.