ros-apartment 3.4.4 → 4.0.0.alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +221 -530
  3. data/config/default.yml +9 -0
  4. data/lib/apartment/CLAUDE.md +76 -251
  5. data/lib/apartment/adapters/CLAUDE.md +34 -161
  6. data/lib/apartment/adapters/abstract_adapter.rb +233 -200
  7. data/lib/apartment/adapters/mysql2_adapter.rb +64 -50
  8. data/lib/apartment/adapters/postgresql_database_adapter.rb +76 -0
  9. data/lib/apartment/adapters/postgresql_schema_adapter.rb +118 -0
  10. data/lib/apartment/adapters/sqlite3_adapter.rb +32 -44
  11. data/lib/apartment/adapters/trilogy_adapter.rb +4 -20
  12. data/lib/apartment/cli/migrations.rb +113 -0
  13. data/lib/apartment/cli/pool.rb +67 -0
  14. data/lib/apartment/cli/seeds.rb +48 -0
  15. data/lib/apartment/cli/tenants.rb +85 -0
  16. data/lib/apartment/cli.rb +18 -0
  17. data/lib/apartment/concerns/model.rb +140 -0
  18. data/lib/apartment/config.rb +217 -0
  19. data/lib/apartment/configs/mysql_config.rb +19 -0
  20. data/lib/apartment/configs/postgresql_config.rb +39 -0
  21. data/lib/apartment/current.rb +18 -0
  22. data/lib/apartment/elevators/CLAUDE.md +49 -10
  23. data/lib/apartment/elevators/first_subdomain.rb +6 -6
  24. data/lib/apartment/elevators/generic.rb +79 -8
  25. data/lib/apartment/elevators/header.rb +26 -0
  26. data/lib/apartment/elevators/host.rb +5 -18
  27. data/lib/apartment/elevators/host_hash.rb +6 -10
  28. data/lib/apartment/elevators/subdomain.rb +8 -27
  29. data/lib/apartment/errors.rb +127 -0
  30. data/lib/apartment/instrumentation.rb +18 -0
  31. data/lib/apartment/lifecycle.rb +32 -0
  32. data/lib/apartment/migrator.rb +210 -37
  33. data/lib/apartment/patches/connection_handling.rb +98 -0
  34. data/lib/apartment/patches/live_tenant_propagation.rb +53 -0
  35. data/lib/apartment/pool_manager.rb +130 -0
  36. data/lib/apartment/pool_reaper.rb +211 -0
  37. data/lib/apartment/railtie.rb +172 -42
  38. data/lib/apartment/schema_cache.rb +26 -0
  39. data/lib/apartment/schema_dumper_patch.rb +48 -0
  40. data/lib/apartment/tasks/CLAUDE.md +16 -93
  41. data/lib/apartment/tasks/v4.rake +50 -0
  42. data/lib/apartment/tenant.rb +323 -45
  43. data/lib/apartment/tenant_name_validator.rb +87 -0
  44. data/lib/apartment/tenant_validator.rb +186 -0
  45. data/lib/apartment/test_fixtures.rb +34 -0
  46. data/lib/apartment/version.rb +1 -1
  47. data/lib/apartment.rb +304 -134
  48. data/lib/generators/apartment/install/install_generator.rb +6 -1
  49. data/lib/generators/apartment/install/templates/apartment.rb +65 -100
  50. data/lib/generators/apartment/install/templates/binstub +6 -0
  51. data/lib/rubocop/apartment.rb +4 -0
  52. data/lib/rubocop/cop/apartment/no_direct_current_write.rb +79 -0
  53. data/lib/rubocop/cop/apartment/prefer_block_switch.rb +39 -0
  54. data/ros-apartment.gemspec +14 -18
  55. metadata +89 -54
  56. data/.gitignore +0 -17
  57. data/.pryrc +0 -5
  58. data/.rspec +0 -4
  59. data/.rubocop.yml +0 -176
  60. data/.ruby-version +0 -1
  61. data/AGENTS.md +0 -19
  62. data/Appraisals +0 -145
  63. data/CLAUDE.md +0 -210
  64. data/CODE_OF_CONDUCT.md +0 -71
  65. data/Gemfile +0 -20
  66. data/Guardfile +0 -11
  67. data/RELEASING.md +0 -106
  68. data/Rakefile +0 -158
  69. data/context7.json +0 -4
  70. data/docs/adapters.md +0 -177
  71. data/docs/architecture.md +0 -274
  72. data/docs/elevators.md +0 -226
  73. data/docs/images/log_example.png +0 -0
  74. data/legacy_CHANGELOG.md +0 -965
  75. data/lib/apartment/active_record/connection_handling.rb +0 -31
  76. data/lib/apartment/active_record/internal_metadata.rb +0 -9
  77. data/lib/apartment/active_record/postgres/schema_dumper.rb +0 -20
  78. data/lib/apartment/active_record/postgresql_adapter.rb +0 -58
  79. data/lib/apartment/active_record/schema_migration.rb +0 -11
  80. data/lib/apartment/adapters/abstract_jdbc_adapter.rb +0 -20
  81. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +0 -19
  82. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +0 -62
  83. data/lib/apartment/adapters/postgis_adapter.rb +0 -13
  84. data/lib/apartment/adapters/postgresql_adapter.rb +0 -325
  85. data/lib/apartment/console.rb +0 -24
  86. data/lib/apartment/custom_console.rb +0 -42
  87. data/lib/apartment/deprecation.rb +0 -8
  88. data/lib/apartment/log_subscriber.rb +0 -45
  89. data/lib/apartment/model.rb +0 -29
  90. data/lib/apartment/tasks/enhancements.rb +0 -122
  91. data/lib/apartment/tasks/schema_dumper.rb +0 -110
  92. data/lib/apartment/tasks/task_helper.rb +0 -292
  93. data/lib/tasks/apartment.rake +0 -133
data/README.md CHANGED
@@ -1,718 +1,409 @@
1
1
  # Apartment
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/ros-apartment.svg)](https://badge.fury.io/rb/ros-apartment)
4
+ [![CI](https://github.com/rails-on-services/apartment/actions/workflows/ci.yml/badge.svg)](https://github.com/rails-on-services/apartment/actions/workflows/ci.yml)
4
5
  [![codecov](https://codecov.io/gh/rails-on-services/apartment/graph/badge.svg?token=Q4I5QL78SA)](https://codecov.io/gh/rails-on-services/apartment)
5
6
 
6
- *Multitenancy for Rails and ActiveRecord*
7
+ *Database-level multitenancy for Rails and ActiveRecord*
7
8
 
8
- Apartment provides tools to help you deal with multiple tenants in your Rails
9
- application. If you need to have certain data sequestered based on account or company,
10
- but still allow some data to exist in a common tenant, Apartment can help.
11
-
12
- ## Apartment Fork: ros-apartment
13
-
14
- This gem is a fork of the original Apartment gem, which is no longer maintained. We have continued development under the name `ros-apartment` to keep the gem up-to-date and compatible with the latest versions of Rails. `ros-apartment` is designed as a drop-in replacement for the original, allowing you to seamlessly transition your application without code changes.
15
-
16
- ## Community Support
17
-
18
- This project thrives on community support. Whether you have an idea for a new feature, find a bug, or need help with `ros-apartment`, we encourage you to participate! For questions and troubleshooting, check out our [Discussions board](https://github.com/rails-on-services/apartment/discussions) to connect with the community. You can also open issues or submit pull requests directly. We are committed to maintaining `ros-apartment` and ensuring it remains a valuable tool for Rails developers.
19
-
20
- ### Maintainer Update
21
-
22
- As of May 2024, Apartment is maintained with the support of [CampusESP](https://www.campusesp.com). We continue to keep Apartment open-source under the MIT license. We also want to recognize and thank the previous maintainers for their valuable contributions to this project.
23
-
24
- ## Installation
25
-
26
- ### Requirements
27
-
28
- - Ruby 3.1+
29
- - Rails 7.0+ (Rails 6.1 support was dropped in v3.4.0)
30
- - PostgreSQL, MySQL, or SQLite3
31
-
32
- ### Rails
33
-
34
- Add the following to your Gemfile:
9
+ Apartment isolates tenant data at the **database level** using PostgreSQL schemas or separate databases — so that tenant data separation is enforced by the database engine, not application code.
35
10
 
36
11
  ```ruby
37
- gem 'ros-apartment', require: 'apartment'
38
- ```
39
-
40
- Then generate your `Apartment` config file using
41
-
42
- ```ruby
43
- bundle exec rails generate apartment:install
44
- ```
45
-
46
- This will create a `config/initializers/apartment.rb` initializer file.
47
- Configure as needed using the docs below.
48
-
49
- That's all you need to set up the Apartment libraries. If you want to switch tenants
50
- on a per-user basis, look under "Usage - Switching tenants per request", below.
51
-
52
- ## Usage
53
-
54
- ### Video Tutorial
55
-
56
- How to separate your application data into different accounts or companies.
57
- [GoRails #47](https://gorails.com/episodes/multitenancy-with-apartment)
58
-
59
- ### Creating new Tenants
60
-
61
- Before you can switch to a new apartment tenant, you will need to create it. Whenever
62
- you need to create a new tenant, you can run the following command:
63
-
64
- ```ruby
65
- Apartment::Tenant.create('tenant_name')
66
- ```
67
-
68
- If you're using the [prepend environment](https://github.com/rails-on-services/apartment#handling-environments) config option or you AREN'T using Postgresql Schemas, this will create a tenant in the following format: "#{environment}\_tenant_name".
69
- In the case of a sqlite database, this will be created in your 'db/' folder. With
70
- other databases, the tenant will be created as a new DB within the system.
71
-
72
- When you create a new tenant, all migrations will be run against that tenant, so it will be
73
- up to date when create returns.
74
-
75
- #### Notes on PostgreSQL
76
-
77
- PostgreSQL works slightly differently than other databases when creating a new tenant. If you
78
- are using PostgreSQL, Apartment by default will set up a new [schema](http://www.postgresql.org/docs/9.3/static/ddl-schemas.html)
79
- and migrate into there. This provides better performance, and allows Apartment to work on systems like Heroku, which
80
- would not allow a full new database to be created.
81
-
82
- One can optionally use the full database creation instead if they want, though this is not recommended
83
-
84
- ### Switching Tenants
85
-
86
- To switch tenants using Apartment, use the following command:
87
-
88
- ```ruby
89
- Apartment::Tenant.switch('tenant_name') do
90
- # ...
12
+ Apartment::Tenant.switch('acme') do
13
+ User.all # only returns users in the 'acme' schema/database
91
14
  end
92
15
  ```
93
16
 
94
- When switch is called, all requests coming to ActiveRecord will be routed to the tenant
95
- you specify (with the exception of excluded models, see below). The tenant is automatically
96
- switched back at the end of the block to what it was before.
97
-
98
- There is also `switch!` which doesn't take a block, but it's recommended to use `switch`.
99
- To return to the default tenant, you can call `switch` with no arguments.
17
+ ## When to Use Apartment
100
18
 
101
- #### Multiple Tenants
19
+ Apartment uses **schema-per-tenant** (PostgreSQL) or **database-per-tenant** (MySQL/SQLite) isolation. This is one of several approaches to multitenancy in Rails. Choose the right one for your situation:
102
20
 
103
- When using schemas, you can also pass in a list of schemas if desired. Any tables defined in a schema earlier in the chain will be referenced first, so this is only useful if you have a schema with only some of the tables defined:
21
+ | Approach | Isolation | Best for | Gem |
22
+ |----------|-----------|----------|-----|
23
+ | **Row-level** (shared tables, `WHERE tenant_id = ?`) | Application-enforced | Many tenants, greenfield apps, cross-tenant reporting | [`acts_as_tenant`](https://github.com/ErwinM/acts_as_tenant) |
24
+ | **Schema-level** (PostgreSQL schemas) | Database-enforced | Fewer high-value tenants, regulatory requirements, retrofitting existing apps | `ros-apartment` |
25
+ | **Database-level** (separate databases) | Full isolation | Strictest isolation, per-tenant performance tuning | `ros-apartment` |
104
26
 
105
- ```ruby
106
- Apartment::Tenant.switch(['tenant_1', 'tenant_2']) do
107
- # ...
108
- end
109
- ```
27
+ **Use Apartment when** you need hard data isolation between tenants — where a missed `WHERE` clause can't accidentally leak data across tenants. This is common in regulated industries, B2B SaaS with contractual isolation requirements, or when retrofitting an existing single-tenant app.
110
28
 
111
- ### Switching Tenants per request
29
+ **Consider row-level tenancy instead** if you have many tenants (hundreds+), need cross-tenant queries, or are starting a greenfield project. Row-level is simpler, uses fewer database resources, and scales more linearly. See the [Arkency comparison](https://blog.arkency.com/comparison-of-approaches-to-multitenancy-in-rails-apps/) for a thorough analysis.
112
30
 
113
- You can have Apartment route to the appropriate tenant by adding some Rack middleware.
114
- Apartment can support many different "Elevators" that can take care of this routing to your data.
31
+ ## About ros-apartment
115
32
 
116
- **NOTE: when switching tenants per-request, keep in mind that the order of your Rack middleware is important.**
117
- See the [Middleware Considerations](#middleware-considerations) section for more.
33
+ This gem is a maintained fork of the original [Apartment gem](https://github.com/influitive/apartment). Maintained by [CampusESP](https://www.campusesp.com) since 2024. Same `require 'apartment'`; v4 introduces a pool-per-tenant architecture that replaces the thread-local switching of v3. Tenant context is fiber-safe via `CurrentAttributes`, and connection pools are managed per tenant rather than swapping search paths on a shared connection. See the [upgrade guide](docs/upgrading-to-v4.md) for migration steps from v3.
118
34
 
119
- The initializer above will generate the appropriate code for the Subdomain elevator
120
- by default. You can see this in `config/initializers/apartment.rb` after running
121
- that generator. If you're *not* using the generator, you can specify your
122
- elevator below. Note that in this case you will **need** to require the elevator
123
- manually in your `application.rb` like so
35
+ ## Installation
124
36
 
125
- ```ruby
126
- # config/application.rb
127
- require 'apartment/elevators/subdomain' # or 'domain', 'first_subdomain', 'host'
128
- ```
37
+ ### Requirements
129
38
 
130
- #### Switch on subdomain
39
+ - Ruby 3.3+
40
+ - Rails 7.2+
41
+ - PostgreSQL 14+, MySQL 8.4+, or SQLite3
131
42
 
132
- In house, we use the subdomain elevator, which analyzes the subdomain of the request and switches to a tenant schema of the same name. It can be used like so:
43
+ ### Setup
133
44
 
134
45
  ```ruby
135
- # application.rb
136
- module MyApplication
137
- class Application < Rails::Application
138
- config.middleware.use Apartment::Elevators::Subdomain
139
- end
140
- end
46
+ # Gemfile
47
+ gem 'ros-apartment', require: 'apartment'
141
48
  ```
142
49
 
143
- If you want to exclude a domain, for example if you don't want your application to treat www like a subdomain, in an initializer in your application, you can set the following:
144
-
145
- ```ruby
146
- # config/initializers/apartment/subdomain_exclusions.rb
147
- Apartment::Elevators::Subdomain.excluded_subdomains = ['www']
50
+ ```bash
51
+ bundle install
52
+ bundle exec rails generate apartment:install
148
53
  ```
149
54
 
150
- This functions much in the same way as Apartment.excluded_models. This example will prevent switching your tenant when the subdomain is www. Handy for subdomains like: "public", "www", and "admin" :)
151
-
152
- #### Switch on first subdomain
55
+ ## Quick Start
153
56
 
154
- To switch on the first subdomain, which analyzes the chain of subdomains of the request and switches to a tenant schema of the first name in the chain (e.g. owls.birds.animals.com would switch to "owls"). It can be used like so:
57
+ The generated initializer at `config/initializers/apartment.rb` configures Apartment:
155
58
 
156
59
  ```ruby
157
- # application.rb
158
- module MyApplication
159
- class Application < Rails::Application
160
- config.middleware.use Apartment::Elevators::FirstSubdomain
161
- end
60
+ Apartment.configure do |config|
61
+ config.tenant_strategy = :schema # :schema (PostgreSQL) or :database_name (MySQL/SQLite)
62
+ config.tenants_provider = -> { Customer.pluck(:subdomain) }
63
+ config.default_tenant = 'public' # auto-defaults for :schema; required for :database_name
162
64
  end
163
65
  ```
164
66
 
165
- If you want to exclude a domain, for example if you don't want your application to treat www like a subdomain, in an initializer in your application, you can set the following:
67
+ Tenant context is block-scoped. Always use `Apartment::Tenant.switch` with a block in application code; it guarantees cleanup on exceptions.
166
68
 
167
69
  ```ruby
168
- # config/initializers/apartment/subdomain_exclusions.rb
169
- Apartment::Elevators::FirstSubdomain.excluded_subdomains = ['www']
170
- ```
70
+ Apartment::Tenant.create('acme')
171
71
 
172
- This functions much in the same way as the Subdomain elevator. **NOTE:** in fact, at the time of this writing, the `Subdomain` and `FirstSubdomain` elevators both use the first subdomain ([#339](https://github.com/influitive/apartment/issues/339#issuecomment-235578610)). If you need to switch on larger parts of a Subdomain, consider using a Custom Elevator.
173
-
174
- #### Switch on domain
175
-
176
- To switch based on full domain (excluding the 'www' subdomains and top level domains *ie '.com'* ) use the following:
177
-
178
- ```ruby
179
- # application.rb
180
- module MyApplication
181
- class Application < Rails::Application
182
- config.middleware.use Apartment::Elevators::Domain
183
- end
72
+ Apartment::Tenant.switch('acme') do
73
+ User.create!(name: 'Alice') # in the 'acme' schema
184
74
  end
185
- ```
186
-
187
- Note that if you have several subdomains, then it will match on the first *non-www* subdomain:
188
- - example.com => example
189
- - www.example.com => example
190
- - a.example.com => a
191
-
192
- #### Switch on full host using a hash
193
75
 
194
- To switch based on full host with a hash to find corresponding tenant name use the following:
195
-
196
- ```ruby
197
- # application.rb
198
- module MyApplication
199
- class Application < Rails::Application
200
- config.middleware.use Apartment::Elevators::HostHash, {'example.com' => 'example_tenant'}
201
- end
202
- end
76
+ Apartment::Tenant.drop('acme')
203
77
  ```
204
78
 
205
- #### Switch on full host, ignoring given first subdomains
79
+ `switch!` exists for console/REPL use but is discouraged in application code.
206
80
 
207
- To switch based on full host to find corresponding tenant name use the following:
81
+ Global models that live outside tenant schemas use `pin_tenant`:
208
82
 
209
83
  ```ruby
210
- # application.rb
211
- module MyApplication
212
- class Application < Rails::Application
213
- config.middleware.use Apartment::Elevators::Host
214
- end
84
+ class Company < ApplicationRecord
85
+ include Apartment::Model
86
+ pin_tenant # always queries the default (public) schema
215
87
  end
216
88
  ```
217
89
 
218
- If you want to exclude a first-subdomain, for example if you don't want your application to include www in the matching, in an initializer in your application, you can set the following:
90
+ ## Configuration Reference
219
91
 
220
- ```ruby
221
- Apartment::Elevators::Host.ignored_first_subdomains = ['www']
222
- ```
92
+ All options are set in `config/initializers/apartment.rb` inside an `Apartment.configure` block.
223
93
 
224
- With the above set, these would be the results:
225
- - example.com => example.com
226
- - www.example.com => example.com
227
- - a.example.com => a.example.com
228
- - www.a.example.com => a.example.com
94
+ ### Required Options
229
95
 
230
- #### Custom Elevator
96
+ `tenant_strategy`: the isolation method. `:schema` for PostgreSQL schema-per-tenant, `:database_name` for MySQL/SQLite database-per-tenant.
231
97
 
232
- A Generic Elevator exists that allows you to pass a `Proc` (or anything that responds to `call`) to the middleware. This Object will be passed in an `ActionDispatch::Request` object when called for you to do your magic. Apartment will use the return value of this proc to switch to the appropriate tenant. Use like so:
98
+ `tenants_provider`: a callable that returns tenant names. Called at migration time and by rake tasks. Example: `-> { Customer.pluck(:subdomain) }`.
233
99
 
234
- ```ruby
235
- # application.rb
236
- module MyApplication
237
- class Application < Rails::Application
238
- # Obviously not a contrived example
239
- config.middleware.use Apartment::Elevators::Generic, proc { |request| request.host.reverse }
240
- end
241
- end
242
- ```
243
-
244
- Your other option is to subclass the Generic elevator and implement your own
245
- switching mechanism. This is exactly how the other elevators work. Look at
246
- the `subdomain.rb` elevator to get an idea of how this should work. Basically
247
- all you need to do is subclass the generic elevator and implement your own
248
- `parse_tenant_name` method that will ultimately return the name of the tenant
249
- based on the request being made. It *could* look something like this:
250
-
251
- ```ruby
252
- # app/middleware/my_custom_elevator.rb
253
- class MyCustomElevator < Apartment::Elevators::Generic
254
-
255
- # @return {String} - The tenant to switch to
256
- def parse_tenant_name(request)
257
- # request is an instance of Rack::Request
100
+ ### Pool Settings
258
101
 
259
- # example: look up some tenant from the db based on this request
260
- tenant_name = SomeModel.from_request(request)
102
+ `tenant_pool_size`: connections per tenant pool (default: 5).
261
103
 
262
- return tenant_name
263
- end
264
- end
265
- ```
104
+ `pool_idle_timeout`: seconds before an idle tenant pool is eligible for reaping (default: 300).
266
105
 
267
- #### Middleware Considerations
106
+ `max_total_connections`: hard cap across all tenant pools; nil for unlimited (default: nil).
268
107
 
269
- In the examples above, we show the Apartment middleware being appended to the Rack stack with
108
+ ### Elevator (Request Tenant Detection)
270
109
 
271
110
  ```ruby
272
- Rails.application.config.middleware.use Apartment::Elevators::Subdomain
111
+ config.elevator = :subdomain
112
+ config.elevator_options = {}
273
113
  ```
274
114
 
275
- By default, the Subdomain middleware switches into a Tenant based on the subdomain at the beginning of the request, and when the request is finished, it switches back to the "public" Tenant. This happens in the [Generic](https://github.com/rails-on-services/apartment/blob/development/lib/apartment/elevators/generic.rb#L22) elevator, so all elevators that inherit from this elevator will operate as such.
115
+ The Railtie auto-inserts elevator middleware after `ActionDispatch::Callbacks` (just before cookies/sessions in full mode; works in API mode too).
276
116
 
277
- It's also good to note that Apartment switches back to the "public" tenant any time an error is raised in your application.
117
+ See the [Elevators](#elevators) section for available options.
278
118
 
279
- This works okay for simple applications, but it's important to consider that you may want to maintain the "selected" tenant through different parts of the Rack application stack. For example, the [Devise](https://github.com/plataformatec/devise) gem adds the `Warden::Manager` middleware at the end of the stack in the examples above, our `Apartment::Elevators::Subdomain` middleware would come after it. Trouble is, Apartment resets the selected tenant after the request is finish, so some redirects (e.g. authentication) in Devise will be run in the context of the "public" tenant. The same issue would also effect a gem such as the [better_errors](https://github.com/charliesome/better_errors) gem which inserts a middleware quite early in the Rails middleware stack.
119
+ ### Migrations
280
120
 
281
- To resolve this issue, consider adding the Apartment middleware at a location in the Rack stack that makes sense for your needs, e.g.:
121
+ `parallel_migration_threads`: number of threads for parallel tenant migration; 0 for sequential (default: 0).
282
122
 
283
- ```ruby
284
- Rails.application.config.middleware.insert_before Warden::Manager, Apartment::Elevators::Subdomain
285
- ```
123
+ `schema_load_strategy`: how to initialize new tenant schemas on create. `nil` (no schema loading), `:schema_rb`, or `:sql` (default: nil).
286
124
 
287
- Now work done in the Warden middleware is wrapped in the `Apartment::Tenant.switch` context started in the Generic elevator.
125
+ `seed_after_create`: run seeds after tenant creation (default: false).
288
126
 
289
- ### Dropping Tenants
127
+ `seed_data_file`: path to a custom seeds file; uses `db/seeds.rb` when nil (default: nil).
290
128
 
291
- To drop tenants using Apartment, use the following command:
129
+ `schema_file`: path to a custom schema file for tenant creation (default: nil).
292
130
 
293
- ```ruby
294
- Apartment::Tenant.drop('tenant_name')
295
- ```
131
+ `check_pending_migrations`: raise `PendingMigrationError` in local environments when a tenant has unapplied migrations (default: true).
296
132
 
297
- When method is called, the schema is dropped and all data from itself will be lost. Be careful with this method.
133
+ ### Advanced
298
134
 
299
- ### Custom Prompt
135
+ `schema_cache_per_tenant`: load per-tenant schema cache files when establishing tenant pools (default: false).
300
136
 
301
- #### Console methods
137
+ `active_record_log`: tag Rails log output with the current tenant using `ActiveSupport::TaggedLogging`. Log lines inside a `switch` block are tagged with `tenant=name`; nested switches stack tags (`[tenant=acme] [tenant=widgets]`). Requires `Rails.logger` to respond to `tagged` (default: false).
302
138
 
303
- `ros-apartment` console configures two helper methods:
304
- 1. `tenant_list` - list available tenants while using the console
305
- 2. `st(tenant_name:String)` - Switches the context to the tenant name passed, if
306
- it exists.
139
+ `sql_query_tags`: add a `tenant` tag to `ActiveRecord::QueryLogs` so SQL queries include a `/* tenant='name' */` comment. Visible in slow query logs, `pg_stat_activity`, and database monitoring tools (default: false).
307
140
 
308
- #### Custom printed prompt
141
+ `shard_key_prefix`: prefix for ActiveRecord shard keys used in tenant pool registration (default: `'apartment'`). Must match `/[a-z_][a-z0-9_]*/`.
309
142
 
310
- `ros-apartment` also has a custom prompt that gives a bit more information about
311
- the context in which you're running. It shows the environment as well as the tenant
312
- that is currently switched to. In order for you to enable this, you need to require
313
- the custom console in your application.
143
+ ### Tenant Naming
314
144
 
315
- In `application.rb` add `require 'apartment/custom_console'`.
316
- Please note that we rely on `pry-rails` to edit the prompt, thus your project needs
317
- to install it as well. In order to do so, you need to add `gem 'pry-rails'` to your
318
- project's gemfile.
145
+ `environmentify_strategy`: how to namespace tenant names per Rails environment. `nil` (no prefix), `:prepend`, `:append`, or a callable (default: nil).
319
146
 
320
- ## Config
147
+ ### RBAC
321
148
 
322
- The following config options should be set up in a Rails initializer such as:
149
+ `migration_role`: a Symbol naming the database role used for migrations (default: nil, uses the connection's default role).
323
150
 
324
- config/initializers/apartment.rb
151
+ `app_role`: a String or callable returning the restricted role for application queries (default: nil).
325
152
 
326
- To set config options, add this to your initializer:
153
+ ### PostgreSQL
327
154
 
328
155
  ```ruby
329
156
  Apartment.configure do |config|
330
- # set your options (described below) here
157
+ config.configure_postgres do |pg|
158
+ pg.persistent_schemas = ['shared_extensions']
159
+ end
331
160
  end
332
161
  ```
333
162
 
334
- ### Skip tenant schema check
335
-
336
- This is configurable by setting: `tenant_presence_check`. It defaults to true
337
- in order to maintain the original gem behavior. This is only checked when using one of the PostgreSQL adapters.
338
- The original gem behavior, when running `switch` would look for the existence of the schema before switching. This adds an extra query on every context switch. While in the default simple scenarios this is a valid check, in high volume platforms this adds some unnecessary overhead which can be detected in some other ways on the application level.
339
-
340
- Setting this configuration value to `false` will disable the schema presence check before trying to switch the context.
163
+ PostgreSQL extensions (hstore, uuid-ossp, etc.) should be installed in a persistent schema so they're accessible from all tenant schemas:
341
164
 
342
165
  ```ruby
343
- Apartment.configure do |config|
344
- config.tenant_presence_check = false
166
+ # lib/tasks/db_enhancements.rake
167
+ namespace :db do
168
+ task extensions: :environment do
169
+ ActiveRecord::Base.connection.execute('CREATE SCHEMA IF NOT EXISTS shared_extensions;')
170
+ ActiveRecord::Base.connection.execute('CREATE EXTENSION IF NOT EXISTS HSTORE SCHEMA shared_extensions;')
171
+ ActiveRecord::Base.connection.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA shared_extensions;')
172
+ end
345
173
  end
174
+
175
+ Rake::Task['db:create'].enhance { Rake::Task['db:extensions'].invoke }
176
+ Rake::Task['db:test:purge'].enhance { Rake::Task['db:extensions'].invoke }
346
177
  ```
347
178
 
348
- ### Additional logging information
179
+ Ensure your `database.yml` includes the persistent schema:
349
180
 
350
- Enabling this configuration will output the database that the process is currently connected to as well as which
351
- schemas are in the search path. This can be enabled by setting to true the `active_record_log` configuration.
181
+ ```yaml
182
+ schema_search_path: "public,shared_extensions"
183
+ ```
352
184
 
353
- Please note that our custom logger inherits from `ActiveRecord::LogSubscriber` so this will be required for the configuration to work.
185
+ Additional PostgreSQL options (set inside the `configure_postgres` block):
354
186
 
355
- **Example log output:**
187
+ `include_schemas_in_dump`: non-public schemas to include in schema dumps, e.g., `%w[ext shared]` (default: []).
356
188
 
357
- <img src="docs/images/log_example.png">
189
+ ### MySQL
358
190
 
359
191
  ```ruby
360
192
  Apartment.configure do |config|
361
- config.active_record_log = true
193
+ config.configure_mysql do |my|
194
+ # MySQL-specific options
195
+ end
362
196
  end
363
197
  ```
364
198
 
365
- ### Excluding models
366
-
367
- If you have some models that should always access the 'public' tenant, you can specify this by configuring Apartment using `Apartment.configure`. This will yield a config object for you. You can set excluded models like so:
368
-
369
- ```ruby
370
- config.excluded_models = ["User", "Company"] # these models will not be multi-tenanted, but remain in the global (public) namespace
371
- ```
372
-
373
- Note that a string representation of the model name is now the standard so that models are properly constantized when reloaded in development
199
+ ## Elevators
374
200
 
375
- Rails will always access the 'public' tenant when accessing these models, but note that tables will be created in all schemas. This may not be ideal, but its done this way because otherwise rails wouldn't be able to properly generate the schema.rb file.
201
+ Elevators are Rack middleware that detect the tenant from the incoming request and call `Apartment::Tenant.switch` for the duration of that request.
376
202
 
377
- > **NOTE - Many-To-Many Excluded Models:**
378
- > Since model exclusions must come from referencing a real ActiveRecord model, `has_and_belongs_to_many` is NOT supported. In order to achieve a many-to-many relationship for excluded models, you MUST use `has_many :through`. This way you can reference the join model in the excluded models configuration.
203
+ Available elevators:
379
204
 
380
- ### Postgresql Schemas
205
+ - Subdomain: `acme.example.com` -> `'acme'`
206
+ - Domain: `acme.com` -> `'acme'`
207
+ - Host: full hostname matching
208
+ - HostHash: `{ 'acme.com' => 'acme_tenant' }`
209
+ - FirstSubdomain: first subdomain in a multi-level chain
210
+ - Header: tenant name from an HTTP header (new in v4)
381
211
 
382
- #### Alternative: Creating new schemas by using raw SQL dumps
383
-
384
- Apartment can be forced to use raw SQL dumps insted of `schema.rb` for creating new schemas. Use this when you are using some extra features in postgres that can't be represented in `schema.rb`, like materialized views etc.
385
-
386
- This only applies while using postgres adapter and `config.use_schemas` is set to `true`.
387
- (Note: this option doesn't use `db/structure.sql`, it creates SQL dump by executing `pg_dump`)
388
-
389
- Enable this option with:
212
+ Configuration via `config.elevator`:
390
213
 
391
214
  ```ruby
392
- config.use_sql = true
393
- ```
394
-
395
- ### Providing a Different default_tenant
396
-
397
- By default, ActiveRecord will use `"$user", public` as the default `schema_search_path`. This can be modified if you wish to use a different default schema be setting:
398
-
399
- ```ruby
400
- config.default_tenant = "some_other_schema"
215
+ Apartment.configure do |config|
216
+ config.elevator = :subdomain
217
+ end
401
218
  ```
402
219
 
403
- With that set, all excluded models will use this schema as the table name prefix instead of `public` and `reset` on `Apartment::Tenant` will return to this schema as well.
404
-
405
- ### Persistent Schemas
220
+ The Railtie inserts the elevator after `ActionDispatch::Callbacks` automatically. In the full middleware stack this places it just before cookies, sessions, and authentication. In API mode (where cookies/sessions are absent), `Callbacks` is still present so the elevator works without changes.
406
221
 
407
- Apartment will normally just switch the `schema_search_path` whole hog to the one passed in. This can lead to problems if you want other schemas to always be searched as well. Enter `persistent_schemas`. You can configure a list of other schemas that will always remain in the search path, while the default gets swapped out:
222
+ If you need different positioning, skip `config.elevator` and insert manually:
408
223
 
409
224
  ```ruby
410
- config.persistent_schemas = ['some', 'other', 'schemas']
225
+ # config/application.rb
226
+ config.middleware.insert_before 'Warden::Manager', Apartment::Elevators::Subdomain
411
227
  ```
412
228
 
413
- ### Installing Extensions into Persistent Schemas
414
-
415
- Persistent Schemas have numerous useful applications. [Hstore](http://www.postgresql.org/docs/9.1/static/hstore.html), for instance, is a popular storage engine for Postgresql. In order to use extensions such as Hstore, you have to install it to a specific schema and have that always in the `schema_search_path`.
416
-
417
- When using extensions, keep in mind:
418
- * Extensions can only be installed into one schema per database, so we will want to install it into a schema that is always available in the `schema_search_path`
419
- * The schema and extension need to be created in the database *before* they are referenced in migrations, database.yml or apartment.
420
- * There does not seem to be a way to create the schema and extension using standard rails migrations.
421
- * Rails db:test:prepare deletes and recreates the database, so it needs to be easy for the extension schema to be recreated here.
422
-
423
- #### 1. Ensure the extensions schema is created when the database is created
229
+ ### Custom Elevator
424
230
 
425
231
  ```ruby
426
- # lib/tasks/db_enhancements.rake
427
-
428
- ####### Important information ####################
429
- # This file is used to setup a shared extensions #
430
- # within a dedicated schema. This gives us the #
431
- # advantage of only needing to enable extensions #
432
- # in one place. #
433
- # #
434
- # This task should be run AFTER db:create but #
435
- # BEFORE db:migrate. #
436
- ##################################################
437
-
438
- namespace :db do
439
- desc 'Also create shared_extensions Schema'
440
- task :extensions => :environment do
441
- # Create Schema
442
- ActiveRecord::Base.connection.execute 'CREATE SCHEMA IF NOT EXISTS shared_extensions;'
443
- # Enable Hstore
444
- ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS HSTORE SCHEMA shared_extensions;'
445
- # Enable UUID-OSSP
446
- ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA shared_extensions;'
447
- # Grant usage to public
448
- ActiveRecord::Base.connection.execute 'GRANT usage ON SCHEMA shared_extensions to public;'
232
+ class MyElevator < Apartment::Elevators::Generic
233
+ def parse_tenant_name(request)
234
+ request.host.split('.').first
449
235
  end
450
236
  end
451
-
452
- Rake::Task["db:create"].enhance do
453
- Rake::Task["db:extensions"].invoke
454
- end
455
-
456
- Rake::Task["db:test:purge"].enhance do
457
- Rake::Task["db:extensions"].invoke
458
- end
459
237
  ```
460
238
 
461
- #### 2. Ensure the schema is in Rails' default connection
239
+ Then pass the class directly:
462
240
 
463
- Next, your `database.yml` file must mimic what you've set for your default and persistent schemas in Apartment. When you run migrations with Rails, it won't know about the extensions schema because Apartment isn't injected into the default connection, it's done on a per-request basis, therefore Rails doesn't know about `hstore` or `uuid-ossp` during migrations. To do so, add the following to your `database.yml` for all environments
464
-
465
- ```yaml
466
- # database.yml
467
- ...
468
- adapter: postgresql
469
- schema_search_path: "public,shared_extensions"
470
- ...
241
+ ```ruby
242
+ config.elevator = MyElevator
471
243
  ```
472
244
 
473
- This would be for a config with `default_tenant` set to `public` and `persistent_schemas` set to `['shared_extensions']`. **Note**: This only works on Heroku with [Rails 4.1+](https://devcenter.heroku.com/changelog-items/426). For apps that use older Rails versions hosted on Heroku, the only way to properly setup is to start with a fresh PostgreSQL instance:
474
-
475
- 1. Append `?schema_search_path=public,hstore` to your `DATABASE_URL` environment variable, by this you don't have to revise the `database.yml` file (which is impossible since Heroku regenerates a completely different and immutable `database.yml` of its own on each deploy)
476
- 2. Run `heroku pg:psql` from your command line
477
- 3. And then `DROP EXTENSION hstore;` (**Note:** This will drop all columns that use `hstore` type, so proceed with caution; only do this with a fresh PostgreSQL instance)
478
- 4. Next: `CREATE SCHEMA IF NOT EXISTS hstore;`
479
- 5. Finally: `CREATE EXTENSION IF NOT EXISTS hstore SCHEMA hstore;` and hit enter (`\q` to exit)
480
-
481
- To double check, login to the console of your Heroku app and see if `Apartment.connection.schema_search_path` is `public,hstore`
245
+ ## Pinned Models (Global Tables)
482
246
 
483
- #### 3. Ensure the schema is in the apartment config
247
+ Models that belong to all tenants (users, companies, plans) are pinned to the default schema:
484
248
 
485
249
  ```ruby
486
- # config/initializers/apartment.rb
487
- ...
488
- config.persistent_schemas = ['shared_extensions']
489
- ...
250
+ class User < ApplicationRecord
251
+ include Apartment::Model
252
+ pin_tenant
253
+ end
490
254
  ```
491
255
 
492
- #### Alternative: Creating schema by default
256
+ Why `pin_tenant`:
493
257
 
494
- Another way that we've successfully configured hstore for our applications is to add it into the
495
- postgresql template1 database so that every tenant that gets created has it by default.
258
+ - Declarative: the model declares its own tenancy, not a distant config list
259
+ - Zeitwerk-safe: no string-to-class resolution at boot time
260
+ - Composable: works with `connected_to(role: :reading)` for read replicas
496
261
 
497
- One caveat with this approach is that it can interfere with other projects in development using the same extensions and template, but not using apartment with this approach.
262
+ Use `has_many :through` for associations between pinned and tenant models. `has_and_belongs_to_many` is not supported across schemas.
498
263
 
499
- You can do so using a command like so
264
+ Pinned models work correctly inside `connected_to(role: :reading)` blocks. The pin bypasses Apartment's tenant routing; Rails' own role routing takes over.
500
265
 
501
- ```bash
502
- psql -U postgres -d template1 -c "CREATE SCHEMA shared_extensions AUTHORIZATION some_username;"
503
- psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS hstore SCHEMA shared_extensions;"
504
- ```
505
-
506
- The *ideal* setup would actually be to install `hstore` into the `public` schema and leave the public
507
- schema in the `search_path` at all times. We won't be able to do this though until public doesn't
508
- also contain the tenanted tables, which is an open issue with no real milestone to be completed.
509
- Happy to accept PR's on the matter.
266
+ For the edge case of models using `connects_to` with a separate database, see [Known Limitations](#known-limitations).
510
267
 
511
- ### Managing Migrations
268
+ ## Callbacks
512
269
 
513
- In order to migrate all of your tenants (or postgresql schemas) you need to provide a list
514
- of dbs to Apartment. You can make this dynamic by providing a Proc object to be called on migrations.
515
- This object should yield an array of string representing each tenant name. Example:
270
+ Hook into tenant lifecycle events:
516
271
 
517
272
  ```ruby
518
- # Dynamically get tenant names to migrate
519
- config.tenant_names = lambda{ Customer.pluck(:tenant_name) }
273
+ Apartment::Adapters::AbstractAdapter.set_callback :create, :after do |adapter|
274
+ # runs after a new tenant is created
275
+ end
520
276
 
521
- # Use a static list of tenant names for migrate
522
- config.tenant_names = ['tenant1', 'tenant2']
277
+ Apartment::Adapters::AbstractAdapter.set_callback :switch, :before do |adapter|
278
+ # runs before switching tenants
279
+ end
523
280
  ```
524
281
 
525
- You can then migrate your tenants using the normal rake task:
282
+ ## Migrations
526
283
 
527
- ```ruby
528
- rake db:migrate
529
- ```
530
-
531
- This just invokes `Apartment::Migrator.migrate(#{tenant_name})` for each tenant name supplied
532
- from `Apartment.tenant_names`
284
+ Rake tasks:
533
285
 
534
- Note that you can disable the default migrating of all tenants with `db:migrate` by setting
535
- `Apartment.db_migrate_tenants = false` in your `Rakefile`. Note this must be done
536
- *before* the rake tasks are loaded. ie. before `YourApp::Application.load_tasks` is called
286
+ - `apartment:create`: create all tenants from `tenants_provider`
287
+ - `apartment:drop`: drop all tenants
288
+ - `apartment:migrate`: run pending migrations on all tenants
289
+ - `apartment:seed`: seed all tenants
290
+ - `apartment:rollback`: rollback last migration on all tenants
537
291
 
538
- #### Parallel Migrations
292
+ The Railtie hooks the primary `db:migrate` task (when defined) so that tenant migrations run after the primary database migrates.
539
293
 
540
- Apartment supports parallel tenant migrations for applications with many schemas where sequential migration time becomes problematic. This is an **advanced feature** that requires understanding of your migration safety guarantees.
294
+ ### Parallel Migrations
541
295
 
542
- ##### Enabling Parallel Migrations
296
+ For applications with many schemas:
543
297
 
544
298
  ```ruby
545
- Apartment.configure do |config|
546
- config.parallel_migration_threads = 4
547
- end
299
+ config.parallel_migration_threads = 4 # 0 = sequential (default)
548
300
  ```
549
301
 
550
- ##### Configuration Options
302
+ Platform notes: parallel migrations use threads. On macOS, libpq has known fork-safety issues, so threads are preferred over processes. Parallel migrations disable PostgreSQL advisory locks; ensure your migrations are safe to run concurrently.
551
303
 
552
- | Option | Default | Description |
553
- |--------|---------|-------------|
554
- | `parallel_migration_threads` | `0` | Number of parallel workers. `0` disables parallelism (recommended default). |
555
- | `parallel_strategy` | `:auto` | `:auto` detects platform, `:threads` forces thread-based, `:processes` forces fork-based. |
556
- | `manage_advisory_locks` | `true` | Disables PostgreSQL advisory locks during parallel execution to prevent deadlocks. |
304
+ ## Known Limitations
557
305
 
558
- ##### Platform Considerations
306
+ ### `connects_to` with Separate Databases
559
307
 
560
- Apartment auto-detects the safest parallelism strategy for your platform:
308
+ If a model (or its abstract base class) uses `connects_to` to point at a completely different database (not just different roles on the same DB), Apartment's `connection_pool` patch will attempt to create a tenant pool for it.
561
309
 
562
- - **Linux**: Uses process-based parallelism (faster due to copy-on-write memory)
563
- - **macOS/Windows**: Uses thread-based parallelism (avoids libpq fork issues)
310
+ Workaround: add `include Apartment::Model` and `pin_tenant` on the abstract class or model that declares `connects_to` to a separate database.
564
311
 
565
- You can override this with `parallel_strategy: :threads` or `parallel_strategy: :processes`, but forcing processes on macOS may cause crashes due to PostgreSQL's C library (libpq) not being fork-safe on that platform.
312
+ The common pattern of `ApplicationRecord` using `connects_to` with multiple roles (writing/reading) on the same database works correctly; Apartment keys pools by `tenant:role` and respects Rails' role routing.
566
313
 
567
- ##### Important: Your Responsibility
314
+ ## ActionController::Live Streaming
568
315
 
569
- When you enable parallel migrations, Apartment disables PostgreSQL advisory locks to prevent deadlocks. This means **you are responsible for ensuring your migrations are safe to run concurrently**.
570
-
571
- **Use parallel migrations when:**
572
- - You have many tenants and sequential migration time is problematic
573
- - Your migrations only modify objects within each tenant's schema
574
- - You've verified your migrations have no cross-schema side effects
575
-
576
- **Stick with sequential execution when:**
577
- - Migrations create or modify PostgreSQL extensions
578
- - Migrations modify shared types, functions, or other database-wide objects
579
- - Migrations have ordering dependencies that span tenants
580
- - You're unsure whether your migrations are parallel-safe
581
-
582
- ##### Connection Pool Sizing
583
-
584
- The `parallel_migration_threads` value should be less than your database connection pool size to avoid exhaustion errors. If you set `parallel_migration_threads: 8`, ensure your `pool` setting in `database.yml` is at least 10 to leave headroom.
585
-
586
- ##### Schema Dump After Migration
587
-
588
- Apartment automatically dumps `schema.rb` after successful migrations, ensuring the dump comes from the public schema (the source of truth). This respects Rails' `dump_schema_after_migration` setting.
589
-
590
- ### Handling Environments
591
-
592
- By default, when not using postgresql schemas, Apartment will prepend the environment to the tenant name
593
- to ensure there is no conflict between your environments. This is mainly for the benefit of your development
594
- and test environments. If you wish to turn this option off in production, you could do something like:
316
+ Apartment v4 handles tenant propagation across `ActionController::Live`'s spawned streaming thread automatically, under both `:thread` and `:fiber` isolation. Including `ActionController::Live` in your controller is sufficient no additional configuration:
595
317
 
596
318
  ```ruby
597
- config.prepend_environment = !Rails.env.production?
598
- ```
599
-
600
- ## Tenants on different servers
601
-
602
- You can store your tenants in different databases on one or more servers.
603
- To do it, specify your `tenant_names` as a hash, keys being the actual tenant names,
604
- values being a hash with the database configuration to use.
319
+ class StreamingController < ApplicationController
320
+ include ActionController::Live
605
321
 
606
- Example:
607
-
608
- ```ruby
609
- config.with_multi_server_setup = true
610
- config.tenant_names = {
611
- 'tenant1' => {
612
- adapter: 'postgresql',
613
- host: 'some_server',
614
- port: 5555,
615
- database: 'postgres' # this is not the name of the tenant's db
616
- # but the name of the database to connect to, before creating the tenant's db
617
- # mandatory in postgresql
618
- }
619
- }
620
- # or using a lambda:
621
- config.tenant_names = lambda do
622
- Tenant.all.each_with_object({}) do |tenant, hash|
623
- hash[tenant.name] = tenant.db_configuration
322
+ def show
323
+ response.headers['Content-Type'] = 'text/event-stream'
324
+ # Apartment::Tenant.current returns the request's tenant here,
325
+ # even though we're now executing on the OS thread Rails spawned
326
+ # for streaming.
327
+ response.stream.write("data: #{{ tenant: Apartment::Tenant.current }.to_json}\n\n")
328
+ ensure
329
+ response.stream.close
624
330
  end
625
331
  end
626
332
  ```
627
333
 
628
- ## Background workers
629
-
630
- Both these gems have been forked as a side consequence of having a new gem name.
631
- You can use them exactly as you were using before. They are, just like this one
632
- a drop-in replacement.
633
-
634
- See [apartment-sidekiq](https://github.com/rails-on-services/apartment-sidekiq)
635
- or [apartment-activejob](https://github.com/rails-on-services/apartment-activejob).
636
-
637
- ## Callbacks
638
-
639
- You can execute callbacks when switching between tenants or creating a new one, Apartment provides the following callbacks:
334
+ How it works: Apartment prepends `ActionController::Live#process` with a patch that backports [rails/rails#56902](https://github.com/rails/rails/pull/56902) to released Rails versions — it points `Thread.current.active_support_execution_state` at the request fiber's hash for the duration of the request, so Rails' own `share_with` carries all `CurrentAttributes` (apartment's tenant plus any app-defined ones) into the spawned streaming thread. User-spawned threads or fibers *inside* a Live action (`Thread.new`, `Async { }`, raw `Fiber.new`) escape the patch and need explicit `Apartment::Tenant.switch` wrapping. See the [upgrading guide](docs/upgrading-to-v4.md) and [`docs/designs/rails-boundary-tenancy.md`](docs/designs/rails-boundary-tenancy.md).
640
335
 
641
- - before_create
642
- - after_create
643
- - before_switch
644
- - after_switch
336
+ ## Background Workers
645
337
 
646
- You can register a callback using [ActiveSupport::Callbacks](https://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html) the following way:
338
+ Use block-scoped switching in jobs:
647
339
 
648
340
  ```ruby
649
- require 'apartment/adapters/abstract_adapter'
650
-
651
- module Apartment
652
- module Adapters
653
- class AbstractAdapter
654
- set_callback :switch, :before do |object|
655
- ...
656
- end
341
+ class TenantJob < ApplicationJob
342
+ def perform(tenant, data)
343
+ Apartment::Tenant.switch(tenant) do
344
+ # process job
657
345
  end
658
346
  end
659
347
  end
660
348
  ```
661
349
 
662
- ## Running rails console without a connection to the database
350
+ For automatic tenant propagation:
663
351
 
664
- By default, once apartment starts, it establishes a connection to the database. It is possible to
665
- disable this initial connection, by running with `APARTMENT_DISABLE_INIT` set to something:
352
+ - [apartment-sidekiq](https://github.com/rails-on-services/apartment-sidekiq)
353
+ - [apartment-activejob](https://github.com/rails-on-services/apartment-activejob)
666
354
 
667
- ```shell
668
- $ APARTMENT_DISABLE_INIT=true DATABASE_URL=postgresql://localhost:1234/buk_development bin/rails runner 'puts 1'
669
- # 1
670
- ```
355
+ A job that forgets to switch runs in the default tenant — for `Rails.cache` and
356
+ other `Tenant.current`-derived resources that silently contaminate another
357
+ tenant's keyspace. Guard routed work with `Apartment::Tenant.require_tenant!`
358
+ (raises unless a real, non-default tenant is active) and pinned/global work with
359
+ `require_default_tenant!`. See [Tenant-Aware Caching](docs/caching.md) for the
360
+ routed-vs-pinned model and the two-store recipe.
671
361
 
672
- ## Contribution Guidelines
362
+ ## Convenience Methods
673
363
 
674
- We welcome and appreciate contributions to `ros-apartment`! Whether you want to report a bug, propose a new feature, or submit a pull request, your help keeps this project thriving. Please review the guidelines below to ensure a smooth collaboration process.
364
+ `Apartment.tenant_names` returns the current tenant list (delegates to `config.tenants_provider.call`). Preserves the v3 API so existing call sites work without changes.
675
365
 
676
- ### How to Contribute
366
+ `Apartment.excluded_models` returns the excluded models list (delegates to `config.excluded_models`). Deprecated in v4; use `Apartment::Model` + `pin_tenant` instead.
677
367
 
678
- 1. **Check Existing Issues and Discussions**
679
- - Before opening a new issue, please check the [issue tracker](https://github.com/rails-on-services/apartment/issues) and our [Discussions board](https://github.com/rails-on-services/apartment/discussions) to see if the topic has already been reported or discussed. This helps us avoid duplication and focus on solving the issue efficiently.
368
+ ## Troubleshooting
680
369
 
681
- 2. **Submitting a Bug Report**
682
- - Ensure your report includes a clear description of the problem, steps to reproduce, and relevant logs or error messages.
683
- - If possible, provide a minimal reproducible example or a failing test case that demonstrates the issue.
370
+ If tenant switching raises unexpected errors, verify that `tenants_provider` returns valid tenant names and that the tenant exists in the database.
684
371
 
685
- 3. **Proposing a Feature**
686
- - For new features, open an issue to discuss your idea before starting development. This allows the maintainers and community to provide feedback and ensure the feature aligns with the project's goals.
687
- - Please be as detailed as possible when describing the feature, its use case, and its potential impact on the existing functionality.
372
+ ## Upgrading from v3
688
373
 
689
- 4. **Submitting a Pull Request**
690
- - Fork the repository and create a feature branch (`git checkout -b my-feature-branch`).
691
- - Follow the existing code style and ensure your changes are well-documented and tested.
692
- - Run the tests locally to verify that your changes do not introduce new issues.
693
- - Use [Appraisal](https://github.com/thoughtbot/appraisal) to test against multiple Rails versions. Ensure all tests pass for supported Rails versions.
694
- - Submit your pull request to the `development` branch, not `main`.
695
- - Include a detailed description of your changes and reference any related issue numbers (e.g., "Fixes #123" or "Closes #456").
374
+ See the [upgrade guide](docs/upgrading-to-v4.md) for a complete list of breaking changes and migration steps.
696
375
 
697
- 5. **Code Review and Merging Process**
698
- - The maintainers will review your pull request and may provide feedback or request changes. We appreciate your patience during this process, as we strive to maintain a high standard for code quality.
699
- - Once approved, your pull request will be merged into the `development` branch. Periodically, we merge the `development` branch into `main` for official releases.
376
+ ## RuboCop cops
700
377
 
701
- 6. **Testing**
702
- - Ensure your code is thoroughly tested. We do not merge code changes without adequate tests. Use RSpec for unit and integration tests.
703
- - If your contribution affects multiple versions of Rails, use Appraisal to verify compatibility across versions.
704
- - Rake tasks (see the Rakefile) are available to help set up your test databases and run tests.
378
+ Apartment ships two optional RuboCop cops that enforce the block-form
379
+ tenant-switching discipline. Enable them in your application's `.rubocop.yml`:
705
380
 
706
- ### Code of Conduct
381
+ ```yaml
382
+ require: rubocop/apartment
383
+ inherit_gem:
384
+ ros-apartment: config/default.yml
385
+ ```
707
386
 
708
- We are committed to providing a welcoming and inclusive environment for all contributors. Please review and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) when participating in the project.
387
+ - **`Apartment/NoDirectCurrentWrite`** (error) bans assigning
388
+ `Apartment::Current.tenant` / `.previous_tenant` directly. Change tenant context
389
+ with `Apartment::Tenant.switch(tenant) { ... }` (or `with_default_tenant` for
390
+ global work), which guarantees a restore via `ensure`.
391
+ - **`Apartment/PreferBlockSwitch`** (warning) — nudges `Apartment::Tenant.switch!`
392
+ toward the block form. `reset` is not flagged.
709
393
 
710
- ### Questions and Support
394
+ Both match the qualified `Apartment::` receiver only. Scope them to your
395
+ application code with the standard `Exclude:` keys if needed. See
396
+ [`docs/designs/rubocop-cops.md`](docs/designs/rubocop-cops.md) for the rationale.
711
397
 
712
- If you have any questions or need support while contributing or using `ros-apartment`, visit our [Discussions board](https://github.com/rails-on-services/apartment/discussions) to ask questions and connect with the maintainer team and community.
398
+ ## Contributing
713
399
 
714
- We look forward to your contributions and thank you for helping us keep `ros-apartment` a reliable and robust tool for the Rails community!
400
+ 1. Check [existing issues](https://github.com/rails-on-services/apartment/issues) and [discussions](https://github.com/rails-on-services/apartment/discussions)
401
+ 2. Fork and create a feature branch
402
+ 3. Write tests: we don't merge without them
403
+ 4. Run `bundle exec rspec spec/unit/` and `bundle exec rubocop`
404
+ 5. Use [Appraisal](https://github.com/thoughtbot/appraisal) to test across Rails versions: `bundle exec appraisal rspec spec/unit/`
405
+ 6. Submit PR to the `main` branch
715
406
 
716
407
  ## License
717
408
 
718
- Apartment remains an open-source project under the [MIT License](http://www.opensource.org/licenses/MIT). We value open-source principles and aim to make multitenancy accessible to all Rails developers.
409
+ [MIT License](http://www.opensource.org/licenses/MIT)