multitenancy-rails 0.0.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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +3 -0
  4. data/AGENTS.md +67 -0
  5. data/CLAUDE.md +67 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +574 -0
  8. data/Rakefile +10 -0
  9. data/docs/README.md +72 -0
  10. data/docs/api.md +130 -0
  11. data/docs/generator.md +88 -0
  12. data/docs/getting-started.md +111 -0
  13. data/docs/integrations.md +145 -0
  14. data/docs/rake-tasks.md +53 -0
  15. data/docs/themes.md +159 -0
  16. data/lib/generators/multitenancy/multitenancy_generator.rb +88 -0
  17. data/lib/generators/multitenancy/templates/application_controller.rb +7 -0
  18. data/lib/generators/multitenancy/templates/home/index.html.erb +2 -0
  19. data/lib/generators/multitenancy/templates/home_controller.rb +9 -0
  20. data/lib/generators/multitenancy/templates/importmap.rb +4 -0
  21. data/lib/generators/multitenancy/templates/javascript/application.js +12 -0
  22. data/lib/generators/multitenancy/templates/javascript/controllers/hello_controller.js +7 -0
  23. data/lib/generators/multitenancy/templates/layouts/application.html.erb +32 -0
  24. data/lib/generators/multitenancy/templates/locales/en.yml +4 -0
  25. data/lib/generators/multitenancy/templates/routes.rb +6 -0
  26. data/lib/generators/multitenancy/templates/stylesheets/application.css +13 -0
  27. data/lib/generators/multitenancy/templates/tailwind/application.css +4 -0
  28. data/lib/multitenancy/controller.rb +25 -0
  29. data/lib/multitenancy/integrations/factory_bot.rb +15 -0
  30. data/lib/multitenancy/integrations/importmap.rb +79 -0
  31. data/lib/multitenancy/integrations/minitest.rb +49 -0
  32. data/lib/multitenancy/integrations/rails.rb +15 -0
  33. data/lib/multitenancy/integrations/rspec.rb +58 -0
  34. data/lib/multitenancy/integrations/tailwind_css.rb +51 -0
  35. data/lib/multitenancy/integrations.rb +12 -0
  36. data/lib/multitenancy/rails.rb +8 -0
  37. data/lib/multitenancy/railtie.rb +32 -0
  38. data/lib/multitenancy/stim.rb +39 -0
  39. data/lib/multitenancy/theme.rb +66 -0
  40. data/lib/multitenancy/version.rb +5 -0
  41. data/lib/multitenancy-rails.rb +1 -0
  42. data/lib/multitenancy.rb +74 -0
  43. data/lib/tasks/multitenancy_tailwindcss.rake +58 -0
  44. metadata +141 -0
data/README.md ADDED
@@ -0,0 +1,574 @@
1
+ # Multitenancy
2
+
3
+ A Rails engine-based multitenancy gem that provides theme isolation through dynamically created Rails engines. Each theme lives in its own directory under `themes/` and gets its own namespace, routes, controllers, views, assets, JavaScript, and locales — all automatically discovered and wired up at boot time.
4
+
5
+ ## Documentation
6
+
7
+ Full guides, theme generator reference, integrations, and rake tasks are published at **[gems.marcosz.com.br/multitenancy-rails](https://gems.marcosz.com.br/multitenancy-rails/)** — part of the [marcosgz Ruby gem catalogue](https://gems.marcosz.com.br).
8
+
9
+ ## Installation
10
+
11
+ Add the gem to your `Gemfile`:
12
+
13
+ ```ruby
14
+ gem "multitenancy-rails"
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ Generate a new theme:
20
+
21
+ ```bash
22
+ bin/rails generate multitenancy my_store
23
+ ```
24
+
25
+ Mount the themes in your routes (`config/routes.rb`):
26
+
27
+ ```ruby
28
+ Rails.application.routes.draw do
29
+ draw(:multitenancy)
30
+
31
+ root "home#index"
32
+ end
33
+ ```
34
+
35
+ Create the route drawing file at `config/routes/multitenancy.rb`:
36
+
37
+ ```ruby
38
+ Multitenancy.themes.each do |theme|
39
+ mount theme.engine, at: "/#{theme.name}"
40
+ end
41
+ ```
42
+
43
+ Start the server and visit `/my-store` to see the theme in action.
44
+
45
+ Or using subdomain
46
+
47
+ ```ruby
48
+ Multitenancy.themes.each do |theme|
49
+ constraints subdomain: theme.name do
50
+ mount theme.engine, at: "/"
51
+ end
52
+ end
53
+ ```
54
+
55
+ Start the server and visit http://my-store.localhost:3000 to see the theme. (You may need to add to /etc/hosts)
56
+
57
+ ## Theme directory structure
58
+
59
+ Each theme is a self-contained directory under `themes/` that mirrors a Rails application structure:
60
+
61
+ ```bash
62
+ themes/my_store/
63
+ ├── app/
64
+ │ ├── controllers/
65
+ │ │ ├── application_controller.rb
66
+ │ │ └── home_controller.rb
67
+ │ ├── views/
68
+ │ │ ├── layouts/
69
+ │ │ │ └── application.html.erb
70
+ │ │ └── home/
71
+ │ │ └── index.html.erb
72
+ │ ├── assets/
73
+ │ │ └── stylesheets/my-store/
74
+ │ │ └── application.css
75
+ │ └── javascript/my-store/ # When using importmap
76
+ │ ├── application.js
77
+ │ └── controllers/
78
+ │ └── hello_controller.js
79
+ ├── config/
80
+ │ ├── routes.rb
81
+ │ ├── importmap.rb # When using importmap
82
+ │ └── locales/
83
+ │ └── en.yml
84
+ ├── spec/
85
+ │ └── factories/ # When using FactoryBot (RSpec)
86
+ └── test/ # When using Minitest
87
+ ├── controllers/
88
+ └── models/
89
+ ```
90
+
91
+ ## How it works
92
+
93
+ ### Boot sequence
94
+
95
+ The gem hooks into Rails through a Railtie and initializes in two phases:
96
+
97
+ **Phase 1 — `before_configuration`:**
98
+
99
+ 1. **Rails integration** discovers all directories under `themes/` and bootstraps each one:
100
+ - Creates a Ruby module namespace (e.g., `Themes::MyStore`)
101
+ - Creates a `Rails::Engine` subclass at `Themes::MyStore::Engine`
102
+ - Registers autoload paths with Zeitwerk (controllers, models, services, etc.)
103
+ - Registers asset paths with Propshaft
104
+ 2. **FactoryBot integration** adds each theme's `spec/factories` to the factory lookup paths
105
+ 3. **RSpec integration** configures theme spec directories for automatic discovery
106
+ 4. **Minitest integration** configures theme test directories for automatic discovery via `rails test`
107
+
108
+ **Phase 2 — `after_initialize`:**
109
+
110
+ 5. **Importmap integration** creates per-theme importmaps with development file watchers
111
+ 6. **TailwindCSS integration** excludes raw Tailwind input files from Propshaft and prepares build output directories
112
+
113
+ ### Theme engine
114
+
115
+ Each theme gets a dynamically created `Rails::Engine` configured via the `Stim` module. The engine provides:
116
+
117
+ - **Isolated namespace** — theme classes live under `Themes::<Name>` and never collide with each other or the main app
118
+ - **Own root path** — the engine's root is the theme directory itself
119
+ - **View paths** — `app/views` within the theme
120
+ - **Asset paths** — `app/assets` registered with Propshaft
121
+ - **Locale paths** — `config/locales` for per-theme I18n
122
+ - **JavaScript paths** — `app/javascript` for importmap/Stimulus support
123
+
124
+ ### Controller concern
125
+
126
+ Theme controllers include `Multitenancy::Controller`, which:
127
+
128
+ - Prepends the theme's `app/views` directory to the view lookup path so theme views take priority
129
+ - Sets `layout "application"` to use the theme's own layout
130
+ - Strips the namespace prefix from controller prefixes so views resolve naturally (e.g., `home/index` instead of `themes/my_store/home/index`)
131
+
132
+ ```ruby
133
+ module Themes::MyStore
134
+ class ApplicationController < ActionController::Base
135
+ include Multitenancy::Controller
136
+ end
137
+ end
138
+ ```
139
+
140
+ ### Routing
141
+
142
+ Themes define their own routes inside their engine:
143
+
144
+ ```ruby
145
+ # themes/my_store/config/routes.rb
146
+ Themes::MyStore::Engine.routes.draw do
147
+ root "home#index"
148
+ end
149
+ ```
150
+
151
+ All theme engines are mounted automatically by the `config/routes/multitenancy.rb` draw file. Each theme is mounted at `/<theme-name>`:
152
+
153
+ | Theme directory | Mount path | Root URL |
154
+ |--------------------|---------------|-------------------------|
155
+ | `themes/my_store` | `/my-store` | `http://localhost:3000/my-store` |
156
+ | `themes/community` | `/community` | `http://localhost:3000/community` |
157
+
158
+ ### Autoload paths
159
+
160
+ The following directories inside a theme are automatically registered with Zeitwerk under the theme's namespace:
161
+
162
+ - `app/controllers`
163
+ - `app/channels`
164
+ - `app/helpers`
165
+ - `app/services`
166
+ - `app/structs`
167
+ - `app/models`
168
+ - `app/mailers`
169
+ - `lib`
170
+
171
+ This means you can add models, services, or any other classes to a theme and they will be autoloaded under the `Themes::<Name>` namespace.
172
+
173
+ ## Generator
174
+
175
+ ```
176
+ bin/rails generate multitenancy NAME [options]
177
+ ```
178
+
179
+ ### Arguments
180
+
181
+ | Argument | Description |
182
+ |----------|-------------|
183
+ | `NAME` | Name of the theme (will be parameterized) |
184
+
185
+ ### Options
186
+
187
+ | Option | Type | Default | Description |
188
+ |-----------------|---------|---------|-------------|
189
+ | `--importmap` | boolean | `true` | Generate importmap configuration and Stimulus setup |
190
+ | `--tailwindcss` | boolean | `false` | Generate Tailwind CSS configuration instead of plain CSS |
191
+
192
+ ### Examples
193
+
194
+ Generate a theme with importmap (default):
195
+
196
+ ```bash
197
+ bin/rails generate multitenancy blog
198
+ ```
199
+
200
+ Generate a theme with Tailwind CSS and importmap:
201
+
202
+ ```bash
203
+ bin/rails generate multitenancy blog --tailwindcss
204
+ ```
205
+
206
+ Generate a theme without importmap:
207
+
208
+ ```bash
209
+ bin/rails generate multitenancy blog --no-importmap
210
+ ```
211
+
212
+ Generate a theme with Tailwind CSS and no importmap:
213
+
214
+ ```bash
215
+ bin/rails generate multitenancy blog --tailwindcss --no-importmap
216
+ ```
217
+
218
+ ## Integrations
219
+
220
+ ### Importmap
221
+
222
+ **Requires:** `importmap-rails` gem
223
+
224
+ Each theme gets its own `Importmap::Map` instance stored on `Themes::<Name>::Engine.importmap`. The integration:
225
+
226
+ 1. Draws the main app's importmap first (Turbo, Stimulus, shared libraries)
227
+ 2. Draws the theme's `config/importmap.rb` on top — theme pins can override app pins
228
+ 3. In development, watches theme JavaScript directories and reloads the importmap on changes
229
+ 4. Removes theme importmap paths from the main app's config to prevent cross-contamination
230
+
231
+ **Theme importmap example** (`themes/blog/config/importmap.rb`):
232
+
233
+ ```ruby
234
+ pin "blog/application", to: "blog/application.js"
235
+
236
+ pin_all_from "themes/blog/app/javascript/blog/controllers",
237
+ under: "blog/controllers", to: "blog/controllers"
238
+ ```
239
+
240
+ **Theme layout usage:**
241
+
242
+ ```erb
243
+ <%= javascript_importmap_tags "blog/application", importmap: Themes::Blog::Engine.importmap %>
244
+ ```
245
+
246
+ **Theme application.js** — loads shared controllers from the main app plus theme-specific ones:
247
+
248
+ ```javascript
249
+ import { Application } from "@hotwired/stimulus"
250
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
251
+
252
+ const application = Application.start()
253
+ application.debug = false
254
+ window.Stimulus = application
255
+
256
+ // Shared controllers from the main app (data-controller="hello")
257
+ eagerLoadControllersFrom("controllers", application)
258
+
259
+ // Theme-specific controllers (overrides shared if same name)
260
+ eagerLoadControllersFrom("blog/controllers", application)
261
+ ```
262
+
263
+ This setup allows themes to use all shared Stimulus controllers from the main app while also defining or overriding controllers specific to the theme.
264
+
265
+ ### Tailwind CSS
266
+
267
+ **Requires:** `tailwindcss-rails` gem (`~> 4.4`)
268
+
269
+ The Tailwind CSS integration compiles per-theme stylesheets using Tailwind CSS v4's CSS-first configuration. No `tailwind.config.js` is needed.
270
+
271
+ **File layout with Tailwind:**
272
+
273
+ ```
274
+ themes/blog/
275
+ └── app/assets/
276
+ ├── tailwind/blog/
277
+ │ └── application.css # Tailwind input (raw @import directives)
278
+ └── builds/blog/
279
+ └── application.css # Compiled output (served by Propshaft)
280
+ ```
281
+
282
+ **Tailwind input file** (`app/assets/tailwind/blog/application.css`):
283
+
284
+ ```css
285
+ @import "tailwindcss";
286
+ @source "../../../views/**/*.html.erb";
287
+ @source "../../../controllers/**/*.rb";
288
+ @source "../../../javascript/blog/**/*.js";
289
+ ```
290
+
291
+ The `@source` directives use relative paths from the input file's location to scan only the theme's own files for utility classes. Each theme is independently scoped.
292
+
293
+ The integration automatically:
294
+ - Excludes the `tailwind/` directory from Propshaft (raw `@import` files cannot be served directly)
295
+ - Creates the `builds/<name>/` output directory
296
+ - Skips entirely if `tailwindcss-rails` is not in the bundle (guard clause)
297
+
298
+ **Layout usage** — no changes needed, the existing `stylesheet_link_tag` resolves to the compiled output:
299
+
300
+ ```erb
301
+ <%= stylesheet_link_tag "blog/application", "data-turbo-track": "reload" %>
302
+ ```
303
+
304
+ #### Rake tasks
305
+
306
+ **`multitenancy:tailwindcss:build`** — compiles Tailwind CSS for all themes:
307
+
308
+ ```bash
309
+ bin/rails multitenancy:tailwindcss:build
310
+ ```
311
+
312
+ This task is automatically hooked into `assets:precompile`, so production deploys (Kamal, Heroku, etc.) compile theme CSS without any extra configuration.
313
+
314
+ **`multitenancy:tailwindcss:watch`** — watches and recompiles on changes (for development):
315
+
316
+ ```bash
317
+ bin/rails multitenancy:tailwindcss:watch
318
+ ```
319
+
320
+ Spawns a separate watcher process per theme. Ctrl-C cleanly terminates all watchers.
321
+
322
+ #### Development setup
323
+
324
+ To run the Tailwind watcher alongside the Rails server during development, use a `Procfile.dev`:
325
+
326
+ ```procfile
327
+ web: bin/rails server
328
+ tailwind: bin/rails multitenancy:tailwindcss:watch
329
+ ```
330
+
331
+ Then start everything with:
332
+
333
+ ```bash
334
+ bin/dev
335
+ ```
336
+
337
+ Or run the watcher in a separate terminal:
338
+
339
+ ```bash
340
+ bin/rails multitenancy:tailwindcss:watch
341
+ ```
342
+
343
+ ### RSpec
344
+
345
+ **Requires:** `rspec-rails` gem
346
+
347
+ The RSpec integration automatically discovers and includes theme specs in test runs:
348
+
349
+ - **`rspec`** (no arguments) — runs `spec/` from the main app plus `spec/` from every theme
350
+ - **`rspec themes/blog`** — expands to `themes/blog/spec` automatically
351
+ - **Nested themes** — if theme names share a prefix (e.g., `blog` and `blog-admin`), running `rspec themes/blog` also includes `themes/blog-admin/spec`
352
+
353
+ No configuration needed. Theme specs are discovered as long as a `spec/` directory exists inside the theme.
354
+
355
+ ### Minitest
356
+
357
+ **Requires:** Rails test runner (included with `railties`)
358
+
359
+ The Minitest integration automatically discovers and includes theme tests in `rails test` runs:
360
+
361
+ - **`bin/rails test`** (no arguments) — runs `test/` from the main app plus `test/` from every theme
362
+ - **`bin/rails test themes/blog`** — expands to `themes/blog/test` automatically
363
+ - **Nested themes** — if theme names share a prefix (e.g., `blog` and `blog-admin`), running `bin/rails test themes/blog` also includes `themes/blog-admin/test`
364
+
365
+ No configuration needed. Theme tests are discovered as long as a `test/` directory exists inside the theme and files follow the `*_test.rb` naming convention.
366
+
367
+ **Theme test directory structure:**
368
+
369
+ ```
370
+ themes/blog/
371
+ └── test/
372
+ ├── controllers/
373
+ │ └── home_controller_test.rb
374
+ └── models/
375
+ └── post_test.rb
376
+ ```
377
+
378
+ **Theme test example:**
379
+
380
+ ```ruby
381
+ # themes/blog/test/controllers/home_controller_test.rb
382
+ require "test_helper"
383
+
384
+ module Themes::Blog
385
+ class HomeControllerTest < ActionDispatch::IntegrationTest
386
+ test "gets root" do
387
+ get blog.root_path
388
+ assert_response :success
389
+ end
390
+ end
391
+ end
392
+ ```
393
+
394
+ ### FactoryBot
395
+
396
+ **Requires:** `factory_bot_rails` gem
397
+
398
+ Automatically adds each theme's `spec/factories` directory to FactoryBot's definition file paths. Theme factories are available in all tests without any extra configuration.
399
+
400
+ ```
401
+ themes/blog/
402
+ └── spec/
403
+ └── factories/
404
+ └── posts.rb # Automatically loaded by FactoryBot
405
+ ```
406
+
407
+ ## Localization
408
+
409
+ Each theme has its own locale files under `config/locales/`. Theme translations are namespaced under `themes.<name>`:
410
+
411
+ ```yaml
412
+ # themes/blog/config/locales/en.yml
413
+ en:
414
+ themes:
415
+ blog:
416
+ hello: "Hello world from the blog theme"
417
+ ```
418
+
419
+ Usage in theme views:
420
+
421
+ ```erb
422
+ <%= t("themes.blog.hello") %>
423
+ ```
424
+
425
+ ## Configuration
426
+
427
+ ### Autoload paths
428
+
429
+ The list of directories that get registered with Zeitwerk for each theme can be customized:
430
+
431
+ ```ruby
432
+ # In an initializer or before configuration
433
+ Multitenancy.config.paths = %w[
434
+ app/controllers
435
+ app/channels
436
+ app/helpers
437
+ app/services
438
+ app/structs
439
+ app/models
440
+ app/mailers
441
+ lib
442
+ ]
443
+ ```
444
+
445
+ Only directories that actually exist within a theme are registered.
446
+
447
+ ### ActiveSupport hook
448
+
449
+ The gem fires an `ActiveSupport` load hook during initialization that other gems or application code can use to hook into:
450
+
451
+ ```ruby
452
+ ActiveSupport.on_load(:multitenancy) do |multitenancy|
453
+ # Custom initialization after all themes are bootstrapped
454
+ end
455
+ ```
456
+
457
+ ## Full example
458
+
459
+ Create a new theme with all integrations:
460
+
461
+ ```bash
462
+ bin/rails generate multitenancy storefront --tailwindcss
463
+ ```
464
+
465
+ This generates:
466
+
467
+ ```
468
+ themes/storefront/
469
+ ├── app/
470
+ │ ├── controllers/
471
+ │ │ ├── application_controller.rb
472
+ │ │ └── home_controller.rb
473
+ │ ├── views/
474
+ │ │ ├── layouts/
475
+ │ │ │ └── application.html.erb
476
+ │ │ └── home/
477
+ │ │ └── index.html.erb
478
+ │ ├── assets/
479
+ │ │ ├── tailwind/storefront/
480
+ │ │ │ └── application.css
481
+ │ │ └── builds/storefront/
482
+ │ │ └── .keep
483
+ │ └── javascript/storefront/
484
+ │ ├── application.js
485
+ │ └── controllers/
486
+ │ └── hello_controller.js
487
+ ├── config/
488
+ │ ├── routes.rb
489
+ │ ├── importmap.rb
490
+ │ └── locales/
491
+ │ └── en.yml
492
+ ```
493
+
494
+ Build and serve:
495
+
496
+ ```bash
497
+ # Compile Tailwind CSS
498
+ bin/rails multitenancy:tailwindcss:build
499
+
500
+ # Start the server
501
+ bin/rails server
502
+
503
+ # Visit http://localhost:3000/storefront
504
+ ```
505
+
506
+ Add a new controller to the theme:
507
+
508
+ ```ruby
509
+ # themes/storefront/app/controllers/products_controller.rb
510
+ module Themes::Storefront
511
+ class ProductsController < ApplicationController
512
+ def index
513
+ @products = Product.all # Access main app models
514
+ end
515
+ end
516
+ end
517
+ ```
518
+
519
+ Add a route:
520
+
521
+ ```ruby
522
+ # themes/storefront/config/routes.rb
523
+ Themes::Storefront::Engine.routes.draw do
524
+ root "home#index"
525
+ resources :products, only: [:index, :show]
526
+ end
527
+ ```
528
+
529
+ Add a Stimulus controller specific to this theme:
530
+
531
+ ```javascript
532
+ // themes/storefront/app/javascript/storefront/controllers/cart_controller.js
533
+ import { Controller } from "@hotwired/stimulus"
534
+
535
+ export default class extends Controller {
536
+ static targets = ["count"]
537
+
538
+ add() {
539
+ this.countTarget.textContent = parseInt(this.countTarget.textContent) + 1
540
+ }
541
+ }
542
+ ```
543
+
544
+ Use it in a theme view — the controller is scoped to this theme only:
545
+
546
+ ```erb
547
+ <!-- themes/storefront/app/views/products/index.html.erb -->
548
+ <div data-controller="storefront--cart">
549
+ <span data-storefront--cart-target="count">0</span>
550
+ <button data-action="storefront--cart#add">Add to cart</button>
551
+ </div>
552
+ ```
553
+
554
+ ## Requirements
555
+
556
+ - Ruby >= 3.1
557
+ - Rails (railties, activesupport)
558
+ - Zeitwerk (for autoloading)
559
+
560
+ ### Optional dependencies
561
+
562
+ | Gem | Integration | Purpose |
563
+ |--------------------|----------------|----------------------------------|
564
+ | `importmap-rails` | Importmap | Per-theme JavaScript with ESM |
565
+ | `tailwindcss-rails` | TailwindCSS | Per-theme Tailwind CSS builds |
566
+ | `rspec-rails` | RSpec | Auto-discover theme specs |
567
+ | `railties` (test runner) | Minitest | Auto-discover theme tests |
568
+ | `factory_bot_rails`| FactoryBot | Auto-discover theme factories |
569
+
570
+ All integrations use guard clauses and are silently skipped when their respective gems are not installed.
571
+
572
+ ## License
573
+
574
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
data/docs/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # multitenancy-rails
2
+
3
+ Engine-based multitenancy for Rails — each tenant is a self-contained Rails engine auto-discovered from `themes/<name>/`.
4
+
5
+ Every theme gets its own controllers, views, routes, assets, JavaScript, locales, factories, and specs. Isolation is enforced by Ruby namespacing and Rails' `isolate_namespace`. The gem hooks into Rails at boot, scans `themes/`, builds a `Rails::Engine` per directory, and wires asset/JS/importmap/Tailwind paths through the Railtie.
6
+
7
+ This gem does **not** handle database-level isolation (schemas, per-tenant databases, or tenant-id scoping). All themes share the main app's database. If you need row-level multitenancy, add it at the ActiveRecord layer; if you need schema-level, pair this gem with one that does that.
8
+
9
+ ## Contents
10
+
11
+ - [Getting started](getting-started.md) — install, generate a theme, mount it
12
+ - [Themes](themes.md) — structure, namespaces, view resolution
13
+ - [Generator](generator.md) — `bin/rails g multitenancy <name>` options
14
+ - [Integrations](integrations.md) — importmap, Tailwind, RSpec, Minitest, FactoryBot
15
+ - [Rake tasks](rake-tasks.md) — theme asset compilation
16
+ - [API reference](api.md)
17
+
18
+ ## Install
19
+
20
+ ```ruby
21
+ # Gemfile
22
+ gem 'multitenancy-rails'
23
+ ```
24
+
25
+ ## One-minute tour
26
+
27
+ ```bash
28
+ bin/rails generate multitenancy storefront --tailwindcss --importmap
29
+ ```
30
+
31
+ Generates:
32
+
33
+ ```
34
+ themes/storefront/
35
+ ├── app/
36
+ │ ├── controllers/{application,home}_controller.rb
37
+ │ ├── views/layouts/application.html.erb
38
+ │ ├── views/home/index.html.erb
39
+ │ ├── assets/tailwind/storefront/application.css
40
+ │ └── javascript/storefront/application.js
41
+ ├── config/routes.rb
42
+ └── config/locales/en.yml
43
+ ```
44
+
45
+ Mount it:
46
+
47
+ ```ruby
48
+ # config/routes.rb
49
+ Rails.application.routes.draw do
50
+ draw(:multitenancy) # reads config/routes/multitenancy.rb
51
+ root 'home#index'
52
+ end
53
+ ```
54
+
55
+ ```ruby
56
+ # config/routes/multitenancy.rb
57
+ Multitenancy.themes.each do |theme|
58
+ mount theme.engine, at: "/#{theme.name}"
59
+ end
60
+ ```
61
+
62
+ Visit `/storefront` — served by `Themes::Storefront::HomeController`.
63
+
64
+ ## Version
65
+
66
+ - Ruby: `>= 3.1`
67
+ - Depends on: `railties`, `activesupport`, `zeitwerk`
68
+ - Optional: `importmap-rails`, `tailwindcss-rails ~> 4.4`, `rspec-rails`, `factory_bot_rails`
69
+
70
+ ## License
71
+
72
+ MIT.