rails-tenantify 0.1.0 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c660926719f56e3805e0d1622366992096727f7cd25e409b6a95fb9c15152cb
4
- data.tar.gz: 3f0c9b560f399bfb374518a575a14c36e3b0489b35ffd3c5e948eb8c6c321335
3
+ metadata.gz: 7564a95a2b8be9ed97bb29115162b1c16140676551f09bb4f2f75247bb7d6c28
4
+ data.tar.gz: 854c340a1a2eb188fff7cc1743d54df24029f636338bdf5ba20c9a743b7492bc
5
5
  SHA512:
6
- metadata.gz: '083c79279cb7edd789d63e134b1a0e0f9d11e2f616468e759b545d6c417675486f25b7efd71f1fb80e15e613c9b46a079480d1791a9816548785bb96873d1d95'
7
- data.tar.gz: 4dc62b900ccaced8c5ee4b652fb750275eda1ca92fcf876632f4efe87b8bdaddf6c4e29384d34bd1e41fee61791748c6c362d630f92cc6ecc658cbbc51261c4f
6
+ metadata.gz: dd44ab9ef42d723a50e76e21d498b3401c150017ae755060c79549f50047af2bc22d874ec5c0e8c651a085371bcf7973a47ee3e2f631bbebb789adaa43fd428b
7
+ data.tar.gz: e693933f5076251b9e0a2053e6c6e0ad7d905dd9bc328f137716ca8fdfe075454ecf775f026c5bcb411b56e3dcef7b8bf107ba5f373cf2be2b01b0bcd7fabd17
data/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.2] - 2026-06-01
6
+
7
+ ### Fixed
8
+
9
+ - Pin `connection_pool` `< 3`, `minitest` `< 6`, and `sqlite3` `< 2` in the Gemfile so Ruby 3.1 CI can run (latest majors require Ruby >= 3.2)
10
+
11
+ ## [0.1.1] - 2026-06-01
12
+
13
+ ### Fixed
14
+
15
+ - Add `lib/rails-tenantify.rb` so Bundler/Rails load the full gem API ([#1](https://github.com/sghani001/rails-tenantify/issues/1))
16
+ - Guard `Tenantify::Railtie` so it always requires `tenantify` first — fixes `undefined method 'configure' for Tenantify:Module` in Rails initializers
17
+
5
18
  ## [0.1.0] - 2026-06-01
6
19
 
7
20
  Published as **`rails-tenantify`** on RubyGems (`gem "rails-tenantify"`). The name `tenantify` is already used by an unrelated gem from 2016.
data/LICENSE CHANGED
File without changes
data/README.md CHANGED
@@ -1,52 +1,109 @@
1
- # rails-tenantify
1
+ # rails-tenantify 🏢
2
2
 
3
- **Modern row-level multi-tenancy for Rails 7+ / Ruby 3.1+**
4
-
5
- The RubyGems package is [`rails-tenantify`](https://rubygems.org/gems/rails-tenantify). The library is required as `tenantify` (same pattern as `rails-persona` → `persona`).
3
+ > Row-level multi-tenancy for Rails scoped models, job-safe context, zero schema-per-tenant complexity.
6
4
 
5
+ [![Gem Version](https://img.shields.io/gem/v/rails-tenantify.svg)](https://rubygems.org/gems/rails-tenantify)
6
+ [![Downloads](https://img.shields.io/gem/dt/rails-tenantify.svg)](https://rubygems.org/gems/rails-tenantify)
7
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
8
  [![CI](https://github.com/sghani001/rails-tenantify/actions/workflows/ci.yml/badge.svg)](https://github.com/sghani001/rails-tenantify/actions/workflows/ci.yml)
9
+ ![Rails](https://img.shields.io/badge/Rails-7.0%2B-red)
10
+ ![Ruby](https://img.shields.io/badge/Ruby-3.1%2B-cc342d)
11
+ ![SQLite](https://img.shields.io/badge/SQLite-compatible-blue)
12
+ ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-compatible-blue)
13
+ ![Stable](https://img.shields.io/badge/stable-0.1.2-brightgreen)
8
14
 
9
- Tenantify is a maintained alternative to `acts_as_tenant`: automatic model scoping, controller resolution, background-job context, bulk-write guards, and test helpersbuilt for Rails 7 and 8.
15
+ **rails-tenantify** is a lightweight Rails gem for **row-level multi-tenancy**. Unlike [apartment](https://github.com/influitive/apartment), which switches entire databases or schemas per tenant, rails-tenantify keeps a single database and scopes records with a foreign key the same model as [acts_as_tenant](https://github.com/ErwinM/acts_as_tenant), but maintained for **Rails 7+**, with **retry-safe jobs**, **bulk-write guards**, and **first-class test helpers**.
10
16
 
11
- ## Installation
17
+ The RubyGems package is [`rails-tenantify`](https://rubygems.org/gems/rails-tenantify). Require the library as `tenantify` (same pattern as `rails-persona` → `persona`).
18
+
19
+ ---
20
+
21
+ ## Compatibility
22
+
23
+ | | Version |
24
+ |---|---|
25
+ | Ruby | >= 3.1 |
26
+ | Rails | >= 7.0 (tested on 7.1) |
27
+ | Database | SQLite3, PostgreSQL, MySQL |
28
+
29
+ ---
12
30
 
13
- Add to your `Gemfile`:
31
+ ## Why rails-tenantify over acts_as_tenant?
32
+
33
+ | | acts_as_tenant | rails-tenantify |
34
+ |---|---|---|
35
+ | Maintenance | Stagnant / issue backlog | Actively maintained |
36
+ | Rails 7 / 8 | Partial | Full |
37
+ | Sidekiq retry loses tenant | Known issue ([#356](https://github.com/ErwinM/acts_as_tenant/issues/356)) | Tenant ID in payload + middleware |
38
+ | `update_all` / `delete_all` scoped | Unreliable | Raises unless intentionally bypassed |
39
+ | Cross-tenant association checks | Manual | Built-in validation |
40
+ | Tenant override protection | None | `:log`, `:raise`, or `:ignore` |
41
+ | API / header resolver | DIY | `set_tenant_by :header` |
42
+ | RSpec helpers | Partial | `with_tenant` / `without_tenant` |
43
+ | Test suite | Aging | RSpec, CI on Ruby 3.1–3.3 |
44
+
45
+ ---
46
+
47
+ ## Installation
14
48
 
15
49
  ```ruby
16
- gem "rails-tenantify"
50
+ gem "rails-tenantify", "~> 0.1.2", require: "rails-tenantify"
17
51
  ```
18
52
 
19
53
  ```bash
20
54
  bundle install
21
55
  ```
22
56
 
57
+ > **Requires v0.1.1+** — fixes `undefined method 'configure' for Tenantify:Module`. Use **v0.1.2+** on Ruby 3.1.
58
+
23
59
  Create `config/initializers/tenantify.rb`:
24
60
 
25
61
  ```ruby
26
62
  Tenantify.configure do |config|
27
63
  config.tenant_model = "Organization"
28
64
  config.on_tenant_not_found = :raise # :raise, :redirect, :null_tenant
29
- config.audit_overrides = :log # :log, :raise, :ignore
65
+ config.audit_overrides = :log # :log, :raise, :ignore
30
66
  end
31
67
  ```
32
68
 
69
+ Add a tenant reference to scoped models (example):
70
+
71
+ ```bash
72
+ rails g migration AddOrganizationToProjects organization:references
73
+ rails db:migrate
74
+ ```
75
+
76
+ ---
77
+
33
78
  ## Quick start
34
79
 
35
- ### Models
80
+ ### 1. Define your tenant model
81
+
82
+ ```ruby
83
+ class Organization < ApplicationRecord
84
+ # e.g. subdomain: "acme" for acme.yourapp.com
85
+ end
86
+ ```
87
+
88
+ ### 2. Scope models to a tenant
36
89
 
37
90
  ```ruby
38
91
  class Project < ApplicationRecord
39
92
  include Tenantify::Scoped
93
+
94
+ belongs_to_tenant :organization
95
+ has_many :tasks
96
+ end
97
+
98
+ class Task < ApplicationRecord
99
+ include Tenantify::Scoped
100
+
40
101
  belongs_to_tenant :organization
102
+ belongs_to :project
41
103
  end
42
104
  ```
43
105
 
44
- - Adds a `default_scope` for the current tenant
45
- - Sets the tenant foreign key on create
46
- - Validates the tenant cannot change after create
47
- - Validates associated records belong to the same tenant
48
-
49
- ### Controllers
106
+ ### 3. Resolve tenant in controllers
50
107
 
51
108
  ```ruby
52
109
  class ApplicationController < ActionController::Base
@@ -57,68 +114,225 @@ class ApplicationController < ActionController::Base
57
114
  end
58
115
  ```
59
116
 
60
- Resolvers live under `Tenantify::Resolvers` (`Subdomain`, `Header`). JWT and custom-domain resolvers are planned for upcoming releases.
117
+ ### 4. Use scoped queries in the request
118
+
119
+ ```ruby
120
+ # Tenantify.current_tenant is set by the controller
121
+ Project.all # => only current organization's projects
122
+ Project.create!(name: "Q2 Roadmap") # organization_id set automatically
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Tenant context
128
+
129
+ ```ruby
130
+ Tenantify.current_tenant # => #<Organization id: 1 ...>
131
+ Tenantify.current_tenant_id # => 1
132
+ Tenantify.tenant_scoped? # => true
133
+
134
+ Tenantify.switch_to(other_org) do
135
+ Project.all # scoped to other_org
136
+ end
137
+ # previous tenant restored
138
+
139
+ Tenantify.without_tenant do
140
+ Project.delete_all # bypasses default scope + bulk guards
141
+ end
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Controller resolvers
147
+
148
+ | Resolver | Usage | Finds tenant by |
149
+ |----------|--------|-----------------|
150
+ | `:subdomain` | `set_tenant_by :subdomain` | `request.subdomain` → `Organization.find_by(subdomain: ...)` |
151
+ | `:header` | `set_tenant_by :header, header: "X-Tenant-ID"` | Header value → `Organization.find_by(id: ...)` |
152
+
153
+ Exclude reserved subdomains:
154
+
155
+ ```ruby
156
+ set_tenant_by :subdomain, exclude: %w[www admin]
157
+ ```
158
+
159
+ When no tenant is found, behavior is controlled by `on_tenant_not_found`:
160
+
161
+ ```ruby
162
+ # :raise → Tenantify::TenantNotFoundError
163
+ # :redirect → redirect_to fallback path
164
+ # :null_tenant → leave current_tenant nil
165
+ set_tenant_by :subdomain, fallback: "/login"
166
+ ```
167
+
168
+ Pluggable classes live under `Tenantify::Resolvers` (`Subdomain`, `Header`).
169
+
170
+ ---
171
+
172
+ ## Background jobs (ActiveJob + Sidekiq)
61
173
 
62
- ### Background jobs
174
+ Tenant context is **serialized when the job is enqueued** and **restored on perform** — including Sidekiq retries.
63
175
 
64
176
  ```ruby
65
177
  class ReportJob < ApplicationJob
66
178
  def perform
67
- Tenantify.current_tenant # restored from enqueue time
179
+ Tenantify.current_tenant # same org as when the job was enqueued
180
+ Project.find_each { |p| p.update!(status: "exported") }
68
181
  end
69
182
  end
183
+
184
+ # In a request:
185
+ Tenantify.current_tenant = current_organization
186
+ ReportJob.perform_later
70
187
  ```
71
188
 
72
- `Tenantify::Job` is included automatically for ActiveJob. Sidekiq workers get tenant metadata via middleware when Sidekiq is present.
189
+ For native Sidekiq workers (non–ActiveJob), middleware injects `tenant_id` into the job hash and wraps execution in `Tenantify.switch_to`.
190
+
191
+ ---
192
+
193
+ ## Bulk-write protection
73
194
 
74
- ### Switching context
195
+ `update_all`, `delete_all`, and `destroy_all` on tenant-scoped models raise `Tenantify::TenantMismatchError` unless the relation is already limited to the current tenant:
75
196
 
76
197
  ```ruby
77
- Tenantify.switch_to(organization) do
78
- Project.all # scoped to organization
79
- end
198
+ Tenantify.current_tenant = org
199
+ Project.update_all(status: "archived") # OK — scoped to org
200
+
201
+ Project.unscoped.update_all(status: "x") # raises TenantMismatchError
80
202
 
81
203
  Tenantify.without_tenant do
82
- Project.delete_all # bypasses bulk-write protection
204
+ Project.update_all(status: "migrated") # OK intentional bypass
83
205
  end
84
206
  ```
85
207
 
86
- ### Tests
208
+ ---
209
+
210
+ ## Cross-tenant association validation
211
+
212
+ ```ruby
213
+ Tenantify.current_tenant = org_a
214
+ project_a = Project.create!(name: "Alpha")
215
+
216
+ task = Task.new(name: "Bad", organization: org_b, project: project_a)
217
+ task.valid? # => false
218
+ task.errors[:project] # => ["belongs to a different tenant"]
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Tenant override auditing
224
+
225
+ ```ruby
226
+ Tenantify.configure { |c| c.audit_overrides = :raise }
227
+
228
+ Tenantify.current_tenant = org_a
229
+ Tenantify.current_tenant = org_b
230
+ # => Tenantify::TenantOverrideError
231
+ ```
232
+
233
+ Use `:log` to warn via `Rails.logger` without raising.
234
+
235
+ ---
236
+
237
+ ## Test helpers (RSpec / Minitest)
87
238
 
88
239
  ```ruby
89
240
  RSpec.configure do |config|
90
241
  config.include Tenantify::TestHelpers
91
242
  end
92
243
 
93
- with_tenant(organization) do
94
- Project.create!(name: "Demo")
244
+ it "creates under a tenant" do
245
+ with_tenant(org_a) do
246
+ project = Project.create!(name: "Demo")
247
+ expect(project.organization_id).to eq(org_a.id)
248
+ end
249
+ end
250
+
251
+ without_tenant do
252
+ Project.delete_all
95
253
  end
254
+
255
+ # Minitest
256
+ setup { Tenantify::TestHelpers.set_tenant(org_a) }
257
+ teardown { Tenantify::TestHelpers.clear_tenant }
96
258
  ```
97
259
 
98
- ## Bulk-write protection
260
+ ---
99
261
 
100
- `update_all`, `delete_all`, and `destroy_all` on tenant-scoped models raise `Tenantify::TenantMismatchError` unless the relation is already scoped to the current tenant, or you use `Tenantify.without_tenant`.
262
+ ## Configuration
101
263
 
102
- ## Errors
264
+ ```ruby
265
+ # config/initializers/tenantify.rb
266
+ Tenantify.configure do |config|
267
+ config.tenant_model = "Organization" # required
268
+ config.on_tenant_not_found = :raise # :raise, :redirect, :null_tenant
269
+ config.audit_overrides = :log # :log, :raise, :ignore
270
+ end
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Comparison with other approaches
276
+
277
+ | Approach | How it works | rails-tenantify advantage |
278
+ |----------|----------------|---------------------------|
279
+ | **acts_as_tenant** | Row-level FK scope | Modern Rails, jobs, bulk guards, maintained |
280
+ | **apartment** | Schema / DB per tenant | Simpler ops — one DB, one migration path |
281
+ | **acts_as_subtenant** | Nested tenants | Flat, explicit `belongs_to_tenant` |
282
+ | **Custom `default_scope`** | Hand-rolled | Override protection, jobs, tests included |
283
+
284
+ ---
285
+
286
+ ## API reference
287
+
288
+ | Method / macro | Description |
289
+ |----------------|-------------|
290
+ | `Tenantify.configure` | Global configuration block |
291
+ | `Tenantify.current_tenant` | Current tenant object (thread-local) |
292
+ | `Tenantify.current_tenant=` | Set tenant (respects `audit_overrides`) |
293
+ | `Tenantify.current_tenant_id` | Current tenant id or `nil` |
294
+ | `Tenantify.tenant_scoped?` | Whether default scope is active |
295
+ | `Tenantify.tenant_class` | Constantized `tenant_model` class |
296
+ | `Tenantify.switch_to(tenant) { }` | Temporary tenant switch |
297
+ | `Tenantify.without_tenant { }` | Disable scoping and bulk guards |
298
+ | `include Tenantify::Scoped` | Model concern for row-level scope |
299
+ | `belongs_to_tenant :association` | FK macro + validations + default scope |
300
+ | `include Tenantify::Controller` | Controller concern |
301
+ | `set_tenant_by :subdomain` | Subdomain resolver |
302
+ | `set_tenant_by :header` | Header resolver |
303
+ | `Tenantify::Job` | ActiveJob tenant serialize / restore (auto-included) |
304
+ | `with_tenant(tenant) { }` | Test helper — block switch |
305
+ | `without_tenant { }` | Test helper — disable scope |
306
+ | `Tenantify::TestHelpers.clear_tenant` | Reset thread-local state |
307
+
308
+ ### Errors
103
309
 
104
310
  | Error | When |
105
311
  |-------|------|
106
312
  | `Tenantify::TenantNotFoundError` | Resolver cannot find a tenant |
107
313
  | `Tenantify::TenantMismatchError` | Unsafe bulk write without tenant scope |
108
314
  | `Tenantify::TenantOverrideError` | Unsafe `current_tenant=` when `audit_overrides` is `:raise` |
315
+ | `Tenantify::Error` | Base error (e.g. missing `tenant_model`) |
316
+
317
+ ---
109
318
 
110
319
  ## Roadmap
111
320
 
112
321
  | Version | Focus |
113
322
  |---------|--------|
323
+ | **0.1.2** (current) | Ruby 3.1 CI — pin `connection_pool` < 3 |
324
+ | **0.1.1** | Fix Rails boot / `Tenantify.configure` entrypoint |
114
325
  | **0.1.0** | Core scoping, subdomain/header resolvers, ActiveJob, Sidekiq, test helpers |
115
326
  | **0.2.0** | GoodJob, Solid Queue |
116
327
  | **0.3.0** | JWT resolver, API improvements |
117
328
  | **0.4.0** | Custom domains, Active Storage |
118
- | **1.0.0** | Stable API, full docs |
329
+ | **0.6.0** | Hotwire / Turbo, GraphQL context |
330
+ | **1.0.0** | Stable API, full documentation |
119
331
 
120
332
  See [CHANGELOG.md](CHANGELOG.md) for release notes.
121
333
 
334
+ ---
335
+
122
336
  ## Development
123
337
 
124
338
  ```bash
@@ -126,10 +340,12 @@ bundle install
126
340
  bundle exec rspec
127
341
  ```
128
342
 
343
+ ---
344
+
129
345
  ## Contributing
130
346
 
131
- Bug reports and pull requests are welcome at [github.com/sghani001/rails-tenantify](https://github.com/sghani001/rails-tenantify).
347
+ Bug reports and pull requests are welcome at https://github.com/sghani001/rails-tenantify.
132
348
 
133
349
  ## License
134
350
 
135
- MIT — see [LICENSE](LICENSE).
351
+ MIT — © Syed M. Ghani
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler requires this file for the +rails-tenantify+ gem (see Gemfile + gemspec name).
4
+ # Without it, Rails may load Tenantify::Railtie alone and define an incomplete Tenantify module.
5
+ require "tenantify"
6
+ require "tenantify/railtie" if defined?(Rails::Railtie)
File without changes
File without changes
File without changes
File without changes
data/lib/tenantify/job.rb CHANGED
File without changes
File without changes
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Rails may load this file before lib/tenantify.rb; ensure the full API is defined.
4
+ require "tenantify" unless Tenantify.respond_to?(:configure)
5
+
3
6
  require "rails/railtie"
4
7
 
5
8
  module Tenantify
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tenantify
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/tenantify.rb CHANGED
@@ -14,7 +14,6 @@ require_relative "tenantify/controller"
14
14
  require_relative "tenantify/job"
15
15
  require_relative "tenantify/switcher"
16
16
  require_relative "tenantify/test_helpers"
17
- require_relative "tenantify/railtie" if defined?(Rails)
18
17
 
19
18
  module Tenantify
20
19
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-tenantify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Syed M. Ghani
@@ -93,7 +93,7 @@ dependencies:
93
93
  version: '1.4'
94
94
  - - "<"
95
95
  - !ruby/object:Gem::Version
96
- version: '3'
96
+ version: '2'
97
97
  type: :development
98
98
  prerelease: false
99
99
  version_requirements: !ruby/object:Gem::Requirement
@@ -103,7 +103,7 @@ dependencies:
103
103
  version: '1.4'
104
104
  - - "<"
105
105
  - !ruby/object:Gem::Version
106
- version: '3'
106
+ version: '2'
107
107
  description: |
108
108
  Tenantify provides row-level multi-tenancy for Rails 7+ applications: model scoping,
109
109
  controller tenant resolution, ActiveJob and Sidekiq context propagation, bulk-write
@@ -117,6 +117,7 @@ files:
117
117
  - CHANGELOG.md
118
118
  - LICENSE
119
119
  - README.md
120
+ - lib/rails-tenantify.rb
120
121
  - lib/tenantify.rb
121
122
  - lib/tenantify/configuration.rb
122
123
  - lib/tenantify/controller.rb
@@ -155,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
156
  - !ruby/object:Gem::Version
156
157
  version: '0'
157
158
  requirements: []
158
- rubygems_version: 3.5.3
159
+ rubygems_version: 3.4.19
159
160
  signing_key:
160
161
  specification_version: 4
161
162
  summary: Modern multi-tenancy for Rails — row-level tenant scoping with jobs, controllers,