The project is in a healthy, maintained state
Schema-based multi-tenant isolation for Rails apps backed by PostgreSQL schemas.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0

Runtime

>= 6.1, < 9.0
>= 6.1, < 9.0
>= 6.1, < 9.0
>= 6.1, < 9.0
 Project Readme

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.