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/docs/api.md ADDED
@@ -0,0 +1,130 @@
1
+ # API Reference
2
+
3
+ ## `Multitenancy` (module)
4
+
5
+ | Method | Description |
6
+ |--------|-------------|
7
+ | `Multitenancy.root` | Rails application root. |
8
+ | `Multitenancy.themes_root` | Path to `themes/` directory. |
9
+ | `Multitenancy.themes` | Array of `Theme` instances, one per `themes/<name>/` directory. Memoized. |
10
+ | `Multitenancy.reset!` | Clear memoized themes. Useful in tests. |
11
+ | `Multitenancy.config` | Global `Config` instance. |
12
+
13
+ Themes are discovered lazily on first access to `.themes` — at Rails boot time they're bootstrapped from the Railtie.
14
+
15
+ ## `Multitenancy::Config`
16
+
17
+ | Attribute | Default | Purpose |
18
+ |-----------|---------|---------|
19
+ | `paths` | 20+ standard Rails paths | Autoload directories to register per theme. |
20
+
21
+ Default `paths`:
22
+
23
+ ```ruby
24
+ %w[
25
+ app/controllers app/channels app/helpers app/services app/structs
26
+ app/models app/mailers app/presenters app/decorators app/queries
27
+ app/resources app/serializers app/transformers app/validators
28
+ app/workers app/jobs app/notifications app/policies lib
29
+ ]
30
+ ```
31
+
32
+ Customize in an initializer:
33
+
34
+ ```ruby
35
+ Multitenancy.config.paths = %w[app/controllers app/models lib]
36
+ ```
37
+
38
+ ## `Multitenancy::Theme`
39
+
40
+ Represents one theme directory.
41
+
42
+ | Method | Description |
43
+ |--------|-------------|
44
+ | `#name` | Theme name (basename of the directory). |
45
+ | `#path` | `Pathname` to the theme's root. |
46
+ | `#namespace` | The dynamically-created `Themes::<Name>` module. |
47
+ | `#engine` | The dynamically-created `Themes::<Name>::Engine` class. |
48
+ | `#bootstrap(app)` | Creates the namespace, builds the engine, registers paths. Called by the Railtie. |
49
+
50
+ You rarely construct `Theme`s yourself — iterate `Multitenancy.themes` instead.
51
+
52
+ ## `Multitenancy::Controller`
53
+
54
+ `ActiveSupport::Concern`. Include in theme application controllers.
55
+
56
+ ```ruby
57
+ # themes/storefront/app/controllers/application_controller.rb
58
+ module Themes::Storefront
59
+ class ApplicationController < ::ApplicationController
60
+ include Multitenancy::Controller
61
+ end
62
+ end
63
+ ```
64
+
65
+ Configures:
66
+
67
+ - `prepend_view_path engine.root.join('app/views')` — theme views win.
68
+ - `layout 'application'` — use the theme's layout.
69
+ - `before_action { engine.importmap_reloader&.execute_if_updated }` — reload on JS changes in dev.
70
+
71
+ ### Private: `_prefixes`
72
+
73
+ Overridden to strip the theme namespace prefix from `controller_path`, so view lookup works naturally:
74
+
75
+ ```
76
+ Themes::Storefront::HomeController → home/index (not themes/storefront/home/index)
77
+ ```
78
+
79
+ ## `Multitenancy::Stim`
80
+
81
+ Internal — the mixin included into each theme's engine class. It configures:
82
+
83
+ - `isolate_namespace`
84
+ - `called_from` (for `Rails::Engine#root`)
85
+ - `paths["app/views"]`
86
+ - `paths["app/assets"]`
87
+ - `paths["config/locales"]`
88
+ - `paths["app/javascript"]`
89
+
90
+ You should not need to interact with it directly.
91
+
92
+ ## `Multitenancy::Integrations`
93
+
94
+ Namespace for optional-gem integrations. Each is a class with a `.call(app)` class method.
95
+
96
+ | Integration | Guarded by | What it does |
97
+ |-------------|------------|--------------|
98
+ | `Rails` | always | Bootstraps every theme (creates namespace, engine, path registration). |
99
+ | `FactoryBot` | `defined?(::FactoryBot)` | Adds each theme's `spec/factories/` to FactoryBot's paths. |
100
+ | `RSpec` | `defined?(::RSpec)` | Expands `rspec themes/<name>` to `themes/<name>/spec`; auto-discovers all theme specs by default. |
101
+ | `Minitest` | `defined?(::Rails::TestUnit::Runner)` | Same discovery behavior for Minitest. |
102
+ | `Importmap` | `defined?(::Importmap::Map)` | Creates per-theme importmaps; removes theme paths from main app's importmap; wires dev reloader. |
103
+ | `TailwindCss` | `defined?(::Tailwindcss::Commands)` | Discovers theme Tailwind inputs, sets builds dirs, excludes raw inputs from Propshaft. |
104
+
105
+ ## Load hook
106
+
107
+ An `ActiveSupport.on_load(:multitenancy)` hook fires after bootstrap completes:
108
+
109
+ ```ruby
110
+ # config/initializers/multitenancy_custom.rb
111
+ ActiveSupport.on_load(:multitenancy) do |multitenancy|
112
+ # multitenancy is the Multitenancy module
113
+ # Multitenancy.themes is fully populated at this point
114
+ end
115
+ ```
116
+
117
+ ## Generator
118
+
119
+ ```bash
120
+ bin/rails generate multitenancy THEME_NAME [--importmap] [--tailwindcss]
121
+ ```
122
+
123
+ See [generator.md](generator.md).
124
+
125
+ ## Rake tasks
126
+
127
+ - `multitenancy:tailwindcss:build`
128
+ - `multitenancy:tailwindcss:watch`
129
+
130
+ See [rake-tasks.md](rake-tasks.md).
data/docs/generator.md ADDED
@@ -0,0 +1,88 @@
1
+ # Generator
2
+
3
+ ```bash
4
+ bin/rails generate multitenancy THEME_NAME [options]
5
+ ```
6
+
7
+ Scaffolds a new theme under `themes/<theme_name>/` with a minimal but runnable structure.
8
+
9
+ ## Options
10
+
11
+ | Flag | Adds |
12
+ |------|------|
13
+ | `--importmap` | `app/javascript/<name>/application.js` and a sample Stimulus controller. |
14
+ | `--tailwindcss` | `app/assets/tailwind/<name>/application.css` with a Tailwind v4 `@import` and a default theme block. |
15
+
16
+ Flags can be combined:
17
+
18
+ ```bash
19
+ bin/rails g multitenancy storefront --importmap --tailwindcss
20
+ ```
21
+
22
+ ## What gets created
23
+
24
+ ### Always
25
+
26
+ ```
27
+ themes/<name>/
28
+ ├── app/controllers/application_controller.rb
29
+ ├── app/controllers/home_controller.rb
30
+ ├── app/views/layouts/application.html.erb
31
+ ├── app/views/home/index.html.erb
32
+ ├── config/routes.rb
33
+ └── config/locales/en.yml
34
+ ```
35
+
36
+ - `ApplicationController` subclasses the main app's `::ApplicationController` and includes `Multitenancy::Controller`.
37
+ - `HomeController` has a single `index` action.
38
+ - `routes.rb` has `root to: 'home#index'`.
39
+
40
+ ### With `--tailwindcss`
41
+
42
+ ```
43
+ themes/<name>/
44
+ └── app/assets/tailwind/<name>/application.css
45
+ ```
46
+
47
+ ```css
48
+ @import 'tailwindcss';
49
+
50
+ @theme {
51
+ /* design tokens here */
52
+ }
53
+ ```
54
+
55
+ A `bin/rails multitenancy:tailwindcss:build` run compiles this to `themes/<name>/app/assets/builds/<name>/application.css`, and it's hooked into `assets:precompile` so production deploys compile it automatically.
56
+
57
+ ### With `--importmap`
58
+
59
+ ```
60
+ themes/<name>/
61
+ └── app/javascript/<name>/
62
+ ├── application.js # Stimulus entrypoint
63
+ └── controllers/
64
+ ├── application.js # Stimulus Application instance
65
+ ├── hello_controller.js # Example controller
66
+ └── index.js # Eager-register controllers
67
+ ```
68
+
69
+ The theme's layout is wired to draw the theme-scoped importmap and `javascript_importmap_tags` tag:
70
+
71
+ ```erb
72
+ <%= javascript_importmap_tags "<name>/application" %>
73
+ ```
74
+
75
+ In development, changes to JS files trigger the theme's importmap reloader automatically (wired by the `Multitenancy::Controller` concern).
76
+
77
+ ## Prerequisites for flags
78
+
79
+ - `--tailwindcss` requires `tailwindcss-rails ~> 4.4`. The gem targets Tailwind CSS v4 (CSS-first config).
80
+ - `--importmap` requires `importmap-rails` in your Gemfile.
81
+
82
+ If a flag's gem isn't present, the generator skips that part silently — but the theme still works, you just won't get the asset/JS scaffolding.
83
+
84
+ ## After generating
85
+
86
+ 1. Mount the theme in `config/routes/multitenancy.rb` (see [getting-started.md](getting-started.md)).
87
+ 2. If you used `--tailwindcss`, run `bin/rails multitenancy:tailwindcss:build` once to generate the compiled CSS.
88
+ 3. If you used `--importmap`, pin any shared libraries in the theme's layout or in the main `config/importmap.rb`.
@@ -0,0 +1,111 @@
1
+ # Getting Started
2
+
3
+ ## Install
4
+
5
+ ```ruby
6
+ # Gemfile
7
+ gem 'multitenancy-rails'
8
+ ```
9
+
10
+ ```bash
11
+ bundle install
12
+ ```
13
+
14
+ ## Generate your first theme
15
+
16
+ ```bash
17
+ bin/rails generate multitenancy storefront
18
+ ```
19
+
20
+ Optional flags:
21
+
22
+ ```bash
23
+ bin/rails generate multitenancy storefront --tailwindcss --importmap
24
+ ```
25
+
26
+ The generator creates `themes/storefront/` with a minimal Rails app structure:
27
+
28
+ ```
29
+ themes/storefront/
30
+ ├── app/
31
+ │ ├── controllers/application_controller.rb # includes Multitenancy::Controller
32
+ │ ├── controllers/home_controller.rb
33
+ │ ├── views/layouts/application.html.erb
34
+ │ └── views/home/index.html.erb
35
+ ├── config/
36
+ │ ├── routes.rb
37
+ │ └── locales/en.yml
38
+ ```
39
+
40
+ See [generator.md](generator.md) for the full list of flags and what they add.
41
+
42
+ ## Mount your themes
43
+
44
+ The gem does not mount themes for you — you decide how they reach the request path.
45
+
46
+ ### Path-based
47
+
48
+ `config/routes.rb`:
49
+
50
+ ```ruby
51
+ Rails.application.routes.draw do
52
+ draw(:multitenancy)
53
+ root 'home#index'
54
+ end
55
+ ```
56
+
57
+ `config/routes/multitenancy.rb`:
58
+
59
+ ```ruby
60
+ Multitenancy.themes.each do |theme|
61
+ mount theme.engine, at: "/#{theme.name}"
62
+ end
63
+ ```
64
+
65
+ `http://localhost:3000/storefront` → `Themes::Storefront::HomeController#index`.
66
+
67
+ ### Subdomain-based
68
+
69
+ ```ruby
70
+ # config/routes/multitenancy.rb
71
+ Multitenancy.themes.each do |theme|
72
+ constraints subdomain: theme.name do
73
+ mount theme.engine, at: '/'
74
+ end
75
+ end
76
+ ```
77
+
78
+ `http://storefront.lvh.me:3000/` → `Themes::Storefront::HomeController#index`.
79
+
80
+ ## Verify
81
+
82
+ ```bash
83
+ bin/rails runner 'pp Multitenancy.themes.map(&:name)'
84
+ # => ["storefront"]
85
+
86
+ bin/rails server
87
+ ```
88
+
89
+ Visit the mount point — you should see the scaffolded `home/index` view.
90
+
91
+ ## What you get per theme
92
+
93
+ Each theme is a fully isolated `Rails::Engine`:
94
+
95
+ - Namespaced under `Themes::<Name>` — classes don't collide with main app
96
+ - Its own view path (prepended to the lookup chain)
97
+ - Its own route namespace
98
+ - Its own `app/assets/` and `app/javascript/` (if importmap enabled)
99
+ - Its own locales (`config/locales/en.yml`)
100
+ - Its own RSpec / Minitest / FactoryBot directories (auto-discovered)
101
+
102
+ What you **don't** get automatically:
103
+
104
+ - Database isolation. Every theme talks to the same database. Scope via `tenant_id` columns or schemas yourself if you need that.
105
+ - Request-scoped tenant context. The theme is determined by the mount point, not by runtime lookup. If you need `Current.tenant`, add it as a `before_action`.
106
+
107
+ ## Next steps
108
+
109
+ - [Themes](themes.md) — anatomy of a theme, namespace rules, view resolution quirks
110
+ - [Integrations](integrations.md) — ES modules, Stimulus, Tailwind v4, spec discovery
111
+ - [API reference](api.md)
@@ -0,0 +1,145 @@
1
+ # Integrations
2
+
3
+ Every integration in this gem is optional and guard-checked: if the underlying gem isn't in your Gemfile, the integration is silently skipped.
4
+
5
+ ## Importmap (`importmap-rails`)
6
+
7
+ ### Per-theme importmaps
8
+
9
+ Each theme gets its own `Importmap::Map` stored on `theme.engine.importmap`. Theme pins don't leak into the main app, and vice versa.
10
+
11
+ The integration also removes theme paths from the main app's importmap config so they don't appear in the root importmap twice:
12
+
13
+ ```
14
+ app.config.importmap.paths.delete_if { |p| p.to_s.start_with?(themes_root) }
15
+ ```
16
+
17
+ ### Development hot-reload
18
+
19
+ A `before_action` in `Multitenancy::Controller` calls `engine.importmap_reloader&.execute_if_updated` on each request. When you edit a file under `themes/<name>/app/javascript/`, the theme's importmap is re-drawn — no server restart needed.
20
+
21
+ ### Drawing the importmap in a layout
22
+
23
+ ```erb
24
+ <%# themes/storefront/app/views/layouts/application.html.erb %>
25
+ <%= javascript_importmap_tags "storefront/application" %>
26
+ ```
27
+
28
+ ### Pinning packages
29
+
30
+ Pin inside the theme's `config/importmap.rb` (same DSL as the main app):
31
+
32
+ ```ruby
33
+ # themes/storefront/config/importmap.rb
34
+ pin 'application', preload: true
35
+ pin '@hotwired/stimulus', to: '@hotwired--stimulus.js'
36
+ pin_all_from 'app/javascript/storefront/controllers', under: 'storefront/controllers'
37
+ ```
38
+
39
+ ## Tailwind CSS (`tailwindcss-rails`, v4 only)
40
+
41
+ ### Compilation
42
+
43
+ Each theme has its own input file (`app/assets/tailwind/<name>/application.css`) compiled to its own output (`app/assets/builds/<name>/application.css`). Tailwind input directories are excluded from Propshaft so unprocessed CSS doesn't leak into the asset pipeline.
44
+
45
+ ### Asset precompile hook
46
+
47
+ ```ruby
48
+ Rake::Task['assets:precompile'].enhance(['multitenancy:tailwindcss:build'])
49
+ ```
50
+
51
+ This is wired automatically by the integration — no setup needed.
52
+
53
+ ### Watch mode (dev)
54
+
55
+ ```bash
56
+ bin/rails multitenancy:tailwindcss:watch
57
+ ```
58
+
59
+ Forks a watcher process per theme, traps `Ctrl-C` to clean up children.
60
+
61
+ See [rake-tasks.md](rake-tasks.md) for task details.
62
+
63
+ ## RSpec (`rspec-rails`)
64
+
65
+ ### Auto-discovery
66
+
67
+ Running `rspec` with no args discovers every theme's `spec/` directory:
68
+
69
+ ```bash
70
+ bundle exec rspec
71
+ ```
72
+
73
+ ### Explicit paths
74
+
75
+ Passing an explicit directory expands it to include the matching theme's `spec/`:
76
+
77
+ ```bash
78
+ bundle exec rspec themes/storefront
79
+ # Expands to: themes/storefront/spec
80
+ ```
81
+
82
+ ### Nested themes
83
+
84
+ If `storefront` and `storefront-admin` exist, `rspec themes/storefront` runs both. Disambiguate by going one level deeper:
85
+
86
+ ```bash
87
+ bundle exec rspec themes/storefront-admin
88
+ ```
89
+
90
+ ## Minitest (`railties` test runner)
91
+
92
+ Mirrors the RSpec integration. Theme `test/` directories are auto-discovered:
93
+
94
+ ```bash
95
+ bin/rails test
96
+ ```
97
+
98
+ Explicit paths expand the same way:
99
+
100
+ ```bash
101
+ bin/rails test themes/storefront # runs themes/storefront/test
102
+ ```
103
+
104
+ ## FactoryBot (`factory_bot_rails`)
105
+
106
+ Theme factories under `themes/<name>/spec/factories/` are added to FactoryBot's definition paths automatically. Build, create, and define as usual:
107
+
108
+ ```ruby
109
+ # themes/storefront/spec/factories/products.rb
110
+ FactoryBot.define do
111
+ factory :storefront_product, class: 'Themes::Storefront::Product' do
112
+ name { 'Widget' }
113
+ end
114
+ end
115
+ ```
116
+
117
+ ## Writing a custom integration
118
+
119
+ Follow the pattern used by every built-in integration: a class under `Multitenancy::Integrations::<Name>` with a `.call(app)` method, guarded against optional-gem absence:
120
+
121
+ ```ruby
122
+ # lib/multitenancy/integrations/my_integration.rb
123
+ module Multitenancy::Integrations
124
+ class MyIntegration
125
+ def self.call(app)
126
+ return unless defined?(::SomeGem)
127
+
128
+ Multitenancy.themes.each do |theme|
129
+ # wire up per-theme stuff here
130
+ end
131
+ end
132
+ end
133
+ end
134
+ ```
135
+
136
+ Hook it into the Railtie in your application:
137
+
138
+ ```ruby
139
+ # config/initializers/multitenancy_custom.rb
140
+ ActiveSupport.on_load(:multitenancy) do
141
+ Multitenancy::Integrations::MyIntegration.call(Rails.application)
142
+ end
143
+ ```
144
+
145
+ The `:multitenancy` load hook fires after every built-in integration has run, so you can safely inspect `Multitenancy.themes` at that point.
@@ -0,0 +1,53 @@
1
+ # Rake Tasks
2
+
3
+ The gem registers tasks under the `multitenancy:` namespace.
4
+
5
+ ## `multitenancy:tailwindcss:build`
6
+
7
+ Compile Tailwind CSS for every theme.
8
+
9
+ ```bash
10
+ bin/rails multitenancy:tailwindcss:build
11
+ ```
12
+
13
+ - Discovers `themes/<name>/app/assets/tailwind/<name>/application.css` inputs.
14
+ - Writes to `themes/<name>/app/assets/builds/<name>/application.css`.
15
+ - Skips themes without a Tailwind input file.
16
+ - Creates the builds directory if missing (so Propshaft picks up the output).
17
+
18
+ This task is wired into `assets:precompile`:
19
+
20
+ ```ruby
21
+ Rake::Task['assets:precompile'].enhance(['multitenancy:tailwindcss:build'])
22
+ ```
23
+
24
+ So production deploys compile theme CSS automatically — no additional config.
25
+
26
+ ## `multitenancy:tailwindcss:watch`
27
+
28
+ Watch mode for development.
29
+
30
+ ```bash
31
+ bin/rails multitenancy:tailwindcss:watch
32
+ ```
33
+
34
+ - Spawns one watcher process per theme.
35
+ - Recompiles on file changes under the theme's Tailwind directory.
36
+ - Traps `SIGINT` / `SIGTERM` to clean up all child PIDs on exit.
37
+
38
+ Runs in the foreground. Use it alongside `bin/dev` or a separate terminal.
39
+
40
+ ## Prerequisites
41
+
42
+ Both tasks require `tailwindcss-rails ~> 4.4`. If the gem isn't in your bundle, the tasks are not registered (no error — they just don't exist).
43
+
44
+ ## Troubleshooting
45
+
46
+ **"Task 'multitenancy:tailwindcss:build' not found"**
47
+ You're missing `tailwindcss-rails` in your Gemfile. Add it (`gem 'tailwindcss-rails', '~> 4.4'`), `bundle install`, restart.
48
+
49
+ **Build output not picked up by asset pipeline**
50
+ The integration excludes `themes/<name>/app/assets/tailwind/` from Propshaft (the raw inputs), but includes `themes/<name>/app/assets/builds/` (the outputs). If you're using Sprockets or Vite, you may need to adjust.
51
+
52
+ **Themes compile to the same path**
53
+ Each theme's output is under its own subdirectory (`builds/<theme-name>/application.css`), so collisions are impossible as long as theme directories have distinct names.
data/docs/themes.md ADDED
@@ -0,0 +1,159 @@
1
+ # Themes
2
+
3
+ A theme is a directory under `themes/` that gets bootstrapped into a `Rails::Engine` at boot.
4
+
5
+ ## Anatomy
6
+
7
+ ```
8
+ themes/storefront/
9
+ ├── app/
10
+ │ ├── controllers/ # Themes::Storefront::XxxController
11
+ │ ├── models/ # Themes::Storefront::Xxx (optional; main app models are also reachable)
12
+ │ ├── views/ # Resolved first, ahead of main app views
13
+ │ ├── helpers/
14
+ │ ├── services/
15
+ │ ├── jobs/
16
+ │ ├── policies/
17
+ │ ├── assets/
18
+ │ ├── javascript/ # If importmap is enabled
19
+ │ └── ...
20
+ ├── config/
21
+ │ ├── routes.rb # Engine-scoped routes
22
+ │ └── locales/
23
+ ├── spec/ # Auto-discovered by RSpec (if present)
24
+ └── test/ # Auto-discovered by Minitest (if present)
25
+ ```
26
+
27
+ Only directories that actually exist are registered. You don't need to create empty `app/mailers/` just for the gem to work.
28
+
29
+ The autoloaded paths by default are:
30
+
31
+ ```
32
+ app/controllers app/channels app/helpers app/services app/structs
33
+ app/models app/mailers app/presenters app/decorators app/queries
34
+ app/resources app/serializers app/transformers app/validators
35
+ app/workers app/jobs app/notifications app/policies lib
36
+ ```
37
+
38
+ Customize in an initializer:
39
+
40
+ ```ruby
41
+ # config/initializers/multitenancy.rb
42
+ Multitenancy.config.paths = %w[app/controllers app/models app/views lib]
43
+ ```
44
+
45
+ ## Namespace rules
46
+
47
+ All Ruby code in a theme lives under `Themes::<Name>`:
48
+
49
+ ```ruby
50
+ # themes/storefront/app/controllers/home_controller.rb
51
+ module Themes::Storefront
52
+ class HomeController < ApplicationController
53
+ def index
54
+ @products = Product.all # main-app model — no namespacing needed
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ The namespace module is created dynamically at boot. You can rely on it existing:
61
+
62
+ ```ruby
63
+ Themes::Storefront # => Themes::Storefront
64
+ Themes::Storefront::Engine # => Themes::Storefront::Engine
65
+ Themes::Storefront::HomeController # => Themes::Storefront::HomeController
66
+ ```
67
+
68
+ ## Engine isolation
69
+
70
+ Each theme's engine runs through `isolate_namespace(Themes::<Name>)`. That means:
71
+
72
+ - Routes are scoped (`home_path` in the engine is NOT `Rails.application.routes.url_helpers.home_path`).
73
+ - Helpers don't leak between themes.
74
+ - Generators inside the theme generate into its namespace.
75
+
76
+ ## Controllers
77
+
78
+ Theme `ApplicationController`s typically include the gem's `Controller` concern:
79
+
80
+ ```ruby
81
+ # themes/storefront/app/controllers/application_controller.rb
82
+ module Themes::Storefront
83
+ class ApplicationController < ::ApplicationController
84
+ include Multitenancy::Controller
85
+ end
86
+ end
87
+ ```
88
+
89
+ The generator does this for you. The concern sets:
90
+
91
+ - `prepend_view_path` — theme views win over main-app views with the same name.
92
+ - `layout "application"` — uses the theme's `layouts/application.html.erb`.
93
+ - A `before_action` that re-executes the theme's importmap reloader in development when JS files change.
94
+
95
+ ### View resolution quirk
96
+
97
+ Without the concern, `Themes::Storefront::HomeController` would look up views under `themes/storefront/home/` — a path that doesn't exist, because views are at `home/`. The concern strips the theme namespace prefix from `_prefixes` so view lookup works naturally:
98
+
99
+ ```
100
+ theme module = Themes::Storefront
101
+ controller = Themes::Storefront::HomeController
102
+ lookup path = home/ (not themes/storefront/home/)
103
+ ```
104
+
105
+ That means `app/views/home/index.html.erb` inside the theme renders for the index action.
106
+
107
+ ## Models
108
+
109
+ Shared across themes by default. You can add theme-specific models in `themes/<name>/app/models/`:
110
+
111
+ ```ruby
112
+ # themes/storefront/app/models/product.rb
113
+ module Themes::Storefront
114
+ class Product < ApplicationRecord
115
+ self.table_name = 'storefront_products'
116
+ end
117
+ end
118
+ ```
119
+
120
+ Inside the theme, `Product` means `Themes::Storefront::Product`. To reach the main app's model, use `::Product`.
121
+
122
+ ## Database
123
+
124
+ All themes share the main app's database connection. If you want per-tenant data, handle it at the model level — a `tenant_id` column plus a default scope, or multiple DB configs in `database.yml` with `connects_to`. The gem doesn't touch this.
125
+
126
+ ## Routes
127
+
128
+ Each theme's `config/routes.rb` is a standard engine routes file:
129
+
130
+ ```ruby
131
+ # themes/storefront/config/routes.rb
132
+ Themes::Storefront::Engine.routes.draw do
133
+ root to: 'home#index'
134
+ resources :products
135
+ end
136
+ ```
137
+
138
+ The main app's `config/routes/multitenancy.rb` decides how each engine gets mounted — see [getting-started.md](getting-started.md).
139
+
140
+ ## Assets & JavaScript
141
+
142
+ - CSS: each theme has its own `app/assets/` paths registered with Propshaft.
143
+ - JS (importmap): each theme has its own `Importmap::Map` with its own pins, separate from the main app. The `javascript_importmap_tags` helper in the theme's layout draws the theme's importmap.
144
+ - Tailwind: each theme has its own input file and gets its own build output under `app/assets/builds/<theme>/`.
145
+
146
+ See [integrations.md](integrations.md) for the asset and JS story in detail.
147
+
148
+ ## Locales
149
+
150
+ `themes/<name>/config/locales/*.yml` is added to `I18n.load_path`. Keys aren't automatically namespaced — if two themes define the same key, the last loaded wins. Scope keys manually if you want them isolated:
151
+
152
+ ```yaml
153
+ # themes/storefront/config/locales/en.yml
154
+ en:
155
+ themes:
156
+ storefront:
157
+ home:
158
+ title: Welcome to Storefront
159
+ ```