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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/AGENTS.md +67 -0
- data/CLAUDE.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +574 -0
- data/Rakefile +10 -0
- data/docs/README.md +72 -0
- data/docs/api.md +130 -0
- data/docs/generator.md +88 -0
- data/docs/getting-started.md +111 -0
- data/docs/integrations.md +145 -0
- data/docs/rake-tasks.md +53 -0
- data/docs/themes.md +159 -0
- data/lib/generators/multitenancy/multitenancy_generator.rb +88 -0
- data/lib/generators/multitenancy/templates/application_controller.rb +7 -0
- data/lib/generators/multitenancy/templates/home/index.html.erb +2 -0
- data/lib/generators/multitenancy/templates/home_controller.rb +9 -0
- data/lib/generators/multitenancy/templates/importmap.rb +4 -0
- data/lib/generators/multitenancy/templates/javascript/application.js +12 -0
- data/lib/generators/multitenancy/templates/javascript/controllers/hello_controller.js +7 -0
- data/lib/generators/multitenancy/templates/layouts/application.html.erb +32 -0
- data/lib/generators/multitenancy/templates/locales/en.yml +4 -0
- data/lib/generators/multitenancy/templates/routes.rb +6 -0
- data/lib/generators/multitenancy/templates/stylesheets/application.css +13 -0
- data/lib/generators/multitenancy/templates/tailwind/application.css +4 -0
- data/lib/multitenancy/controller.rb +25 -0
- data/lib/multitenancy/integrations/factory_bot.rb +15 -0
- data/lib/multitenancy/integrations/importmap.rb +79 -0
- data/lib/multitenancy/integrations/minitest.rb +49 -0
- data/lib/multitenancy/integrations/rails.rb +15 -0
- data/lib/multitenancy/integrations/rspec.rb +58 -0
- data/lib/multitenancy/integrations/tailwind_css.rb +51 -0
- data/lib/multitenancy/integrations.rb +12 -0
- data/lib/multitenancy/rails.rb +8 -0
- data/lib/multitenancy/railtie.rb +32 -0
- data/lib/multitenancy/stim.rb +39 -0
- data/lib/multitenancy/theme.rb +66 -0
- data/lib/multitenancy/version.rb +5 -0
- data/lib/multitenancy-rails.rb +1 -0
- data/lib/multitenancy.rb +74 -0
- data/lib/tasks/multitenancy_tailwindcss.rake +58 -0
- 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
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.
|