plutonium 0.50.0 → 0.51.0

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
data/docs/guides/index.md CHANGED
@@ -1,52 +1,32 @@
1
1
  # Guides
2
2
 
3
- Task-oriented guides for common Plutonium operations.
3
+ Task-oriented walkthroughs. Each guide shows you how to accomplish a specific task, step-by-step, with links to [Reference](/reference/) for the concepts.
4
4
 
5
- ## Getting Things Done
5
+ ## I want to…
6
6
 
7
- These guides show you how to accomplish specific tasks, with complete examples.
8
-
9
- ### Setup & Resources
10
-
11
- - [Adding Resources](./adding-resources) - Create and connect resources to portals
12
- - [Creating Packages](./creating-packages) - Organize code into feature and portal packages
13
-
14
- ### Authentication & Authorization
15
-
16
- - [Authentication](./authentication) - Set up user authentication with Rodauth
17
- - [Authorization](./authorization) - Implement policies for access control
18
-
19
- ### Features
20
-
21
- - [Custom Actions](./custom-actions) - Add interactive actions with Interactions
22
- - [Nested Resources](./nested-resources) - Parent/child resource relationships
23
- - [Multi-tenancy](./multi-tenancy) - Scope data to organizations or accounts
24
- - [Search and Filtering](./search-filtering) - Implement search, filters, and scopes
25
- - [User Invites](./user-invites) - Invitation system for multi-tenant apps
26
-
27
- ### Customization
28
-
29
- - [Theming](./theming) - Customize colors, styles, and branding
30
-
31
- ### Help
32
-
33
- - [Troubleshooting](./troubleshooting) - Common issues and solutions
34
-
35
- ## Finding What You Need
36
-
37
- | I want to... | Guide |
38
- |--------------|-------|
39
- | Add a new model to my app | [Adding Resources](./adding-resources) |
7
+ | Task | Guide |
8
+ |---|---|
9
+ | Add a new model to my app | [Adding resources](./adding-resources) |
10
+ | Organize code across multiple teams | [Creating packages](./creating-packages) |
40
11
  | Protect pages with login | [Authentication](./authentication) |
41
12
  | Control who can edit what | [Authorization](./authorization) |
42
- | Add a "Publish" button | [Custom Actions](./custom-actions) |
43
- | Show comments under posts | [Nested Resources](./nested-resources) |
13
+ | Add a "Publish" button | [Custom actions](./custom-actions) |
14
+ | Show comments under posts | [Nested resources](./nested-resources) |
44
15
  | Separate data by company | [Multi-tenancy](./multi-tenancy) |
45
- | Add search to a list | [Search and Filtering](./search-filtering) |
46
- | Invite users to an org | [User Invites](./user-invites) |
47
- | Change the color scheme | [Theming](./theming) |
16
+ | Add search to a list | [Search and filtering](./search-filtering) |
17
+ | Invite users to an organization | [User invites](./user-invites) |
18
+ | Add a profile / account-settings page | [User profile](./user-profile) |
19
+ | Customize colors, branding, styles | [Theming](./theming) |
20
+ | Write tests for resources | [Testing](./testing) |
48
21
  | Fix a confusing error | [Troubleshooting](./troubleshooting) |
49
22
 
50
- ## Looking for Reference Docs?
23
+ ## Guides vs Reference
24
+
25
+ - **Guides** = "how do I do X?" — step-by-step, opinionated, narrative.
26
+ - **[Reference](/reference/)** = "what does X do?" — exhaustive option lookup, concept catalog.
27
+
28
+ When a guide says "see Reference › Foo", it's pointing to the catalog page for the full option/method/DSL list.
29
+
30
+ ## New to Plutonium?
51
31
 
52
- For complete API documentation, see the [Reference](/reference/) section.
32
+ Start with the [Tutorial](/getting-started/tutorial/) — an 8-step walkthrough that builds a blog with auth, authorization, custom actions, nested resources, and a customer portal.
@@ -1,48 +1,43 @@
1
1
  # Multi-tenancy
2
2
 
3
- This guide covers isolating data by organization, account, or other entity.
3
+ Isolate data by organization, account, or any other "entity". Plutonium handles the URL strategy, query scoping, form injection, and `belongs_to` auto-detection automatically.
4
4
 
5
- ## Overview
5
+ ## Goal
6
6
 
7
- Multi-tenancy means each tenant (organization, company, account) sees only their own data. Plutonium supports this through:
7
+ Each tenant sees only their own records. Queries are filtered, forms inject the tenant on create, URLs include the tenant id, and policies receive the tenant for authorization.
8
8
 
9
- - **Entity Scoping** - Automatic query filtering via portal configuration
10
- - **Path or Custom Strategies** - Flexible entity resolution
11
- - **Policy Integration** - Authorization automatically respects tenancy
9
+ ## 🚨 Critical
12
10
 
13
- ## Setting Up Multi-tenancy
11
+ - **Never bypass `default_relation_scope`.** Overriding `relation_scope` with `where(organization: ...)` or manual joins triggers `verify_default_relation_scope_applied!` at runtime. Always call `default_relation_scope(relation)` explicitly — not `super`.
12
+ - **Always declare an association path from the model to the entity.** Direct `belongs_to`, `has_one :through`, or a custom `associated_with_<entity>` scope. If `associated_with` can't resolve, fix the **model**, not the policy.
13
+ - **Compound uniqueness scoped to the tenant FK.** `validates :code, uniqueness: {scope: :organization_id}` — without this, uniqueness leaks across tenants.
14
14
 
15
- ### 1. Create the Entity Model
15
+ ## Quickest path: `pu:saas:setup`
16
16
 
17
- ```ruby
18
- # app/models/organization.rb
19
- class Organization < ApplicationRecord
20
- include Plutonium::Resource::Record
21
-
22
- has_many :users
23
- has_many :posts
24
- end
17
+ ```bash
18
+ rails g pu:saas:setup --user Customer --entity Organization
25
19
  ```
26
20
 
27
- ### 2. Add Entity Reference to Resources
21
+ This **meta-generator** creates the user + entity + membership trio AND runs `pu:saas:portal`, `pu:profile:setup`, `pu:saas:welcome`, and `pu:invites:install` in one shot. The portal is fully wired for entity scoping.
28
22
 
29
- Resources must have an association path to the entity:
23
+ See [Reference Auth Accounts SaaS setup](/reference/auth/accounts#saas-setup-pu-saas-setup).
30
24
 
31
- ```ruby
32
- # Direct association (preferred)
33
- class Post < ResourceRecord
34
- belongs_to :organization
35
- belongs_to :user
36
- end
25
+ ## Manual setup
37
26
 
38
- # Through association
39
- class Comment < ResourceRecord
40
- belongs_to :post
41
- has_one :organization, through: :post
42
- end
27
+ ### 1. Create the entity model
28
+
29
+ ```bash
30
+ rails g pu:res:scaffold Organization name:string:uniq slug:string:uniq --dest=main_app
43
31
  ```
44
32
 
45
- ### 3. Configure the Portal Engine
33
+ ### 2. Add the FK to each tenant-scoped resource
34
+
35
+ ```bash
36
+ rails g pu:res:scaffold Post organization:belongs_to title:string content:text --dest=main_app
37
+ rails db:migrate
38
+ ```
39
+
40
+ ### 3. Scope the portal to the entity
46
41
 
47
42
  ```ruby
48
43
  # packages/customer_portal/lib/engine.rb
@@ -51,252 +46,186 @@ module CustomerPortal
51
46
  include Plutonium::Portal::Engine
52
47
 
53
48
  config.after_initialize do
54
- # Path strategy - entity ID in URL
55
49
  scope_to_entity Organization, strategy: :path
56
50
  end
57
51
  end
58
52
  end
59
53
  ```
60
54
 
61
- Routes become: `/organizations/:organization_id/posts`
62
-
63
- ## Scoping Strategies
55
+ Or pass `--scope=Organization` to `pu:pkg:portal` and the engine wires this automatically.
64
56
 
65
- ### Path Strategy (Default)
66
-
67
- Entity ID is included in the URL path:
57
+ ### 4. Mount the portal
68
58
 
69
59
  ```ruby
70
- config.after_initialize do
71
- scope_to_entity Organization, strategy: :path
72
- end
60
+ # config/routes.rb
61
+ mount CustomerPortal::Engine, at: "/customer"
73
62
  ```
74
63
 
75
- The user must have access to the organization (via `associated_with` scope).
64
+ URLs now include the entity id: `/customer/organizations/42/posts`.
76
65
 
77
- ### Custom Strategy
78
-
79
- Define a method that returns the current entity:
66
+ ### 5. Compound uniqueness
80
67
 
81
68
  ```ruby
82
- # packages/customer_portal/lib/engine.rb
83
- config.after_initialize do
84
- scope_to_entity Organization, strategy: :current_organization
85
- end
86
- ```
87
-
88
- ```ruby
89
- # packages/customer_portal/app/controllers/customer_portal/concerns/controller.rb
90
- module CustomerPortal
91
- module Concerns
92
- module Controller
93
- extend ActiveSupport::Concern
94
- include Plutonium::Portal::Controller
95
- include Plutonium::Auth::Rodauth(:user)
96
-
97
- private
98
-
99
- # Method name must match strategy
100
- def current_organization
101
- @current_organization ||= current_user.organization
102
- end
103
- end
104
- end
69
+ class Post < ResourceRecord
70
+ belongs_to :organization
71
+ validates :slug, uniqueness: {scope: :organization_id}
105
72
  end
106
73
  ```
107
74
 
108
- ## How Entity Scoping Works
75
+ 🚨 Without the `scope:`, the same slug in different orgs would collide.
109
76
 
110
- ### Automatic Query Filtering
77
+ ## Strategies
111
78
 
112
- All resource queries are automatically scoped via `associated_with`:
79
+ ### Path strategy (default)
113
80
 
114
81
  ```ruby
115
- # In a scoped portal
116
- Post.all # Returns only current entity's posts
82
+ scope_to_entity Organization, strategy: :path
83
+ # /organizations/:organization_id/posts
117
84
  ```
118
85
 
119
- ### Helper Methods
120
-
121
- Inside controllers:
86
+ ### Custom param key
122
87
 
123
88
  ```ruby
124
- current_scoped_entity # The current Organization/Account/etc.
125
- scoped_to_entity? # true if scoping is active
126
- scoped_entity_class # Organization (the entity class)
89
+ scope_to_entity Organization, strategy: :path, param_key: :org_id
90
+ # /orgs/:org_id/posts
127
91
  ```
128
92
 
129
- ### Model Requirements
130
-
131
- Models must have an association path to the scoped entity. Plutonium automatically resolves:
132
-
133
- 1. **Direct belongs_to** - `Post belongs_to :organization`
134
- 2. **Through association** - `Comment has_one :organization, through: :post`
135
- 3. **Custom scope** - For complex cases, define a named scope:
93
+ ### Subdomain / session / custom
136
94
 
137
95
  ```ruby
138
- class AuditLog < ResourceRecord
139
- # When automatic resolution fails, define this scope
140
- scope :associated_with_organization, ->(org) {
141
- joins(:user).where(users: { organization_id: org.id })
142
- }
143
- end
96
+ scope_to_entity Organization, strategy: :current_organization
144
97
  ```
145
98
 
146
- ## User Membership Patterns
147
-
148
- ### Single Organization per User
99
+ Then implement the method on the portal's controller concern:
149
100
 
150
101
  ```ruby
151
- class User < ApplicationRecord
152
- belongs_to :organization
153
- end
102
+ module CustomerPortal::Concerns::Controller
103
+ extend ActiveSupport::Concern
104
+ include Plutonium::Portal::Controller
105
+
106
+ private
154
107
 
155
- # Custom strategy
156
- def current_organization
157
- current_user.organization
108
+ def current_organization
109
+ @current_organization ||= Organization.find_by!(subdomain: request.subdomain)
110
+ end
158
111
  end
159
112
  ```
160
113
 
161
- ### Multiple Organizations per User
114
+ ## Three model shapes
162
115
 
163
- ```ruby
164
- class User < ApplicationRecord
165
- has_many :memberships
166
- has_many :organizations, through: :memberships
167
- end
116
+ How tenant scoping resolves depends on how the model relates to the entity. Three shapes, pick the lightest:
168
117
 
169
- # Custom strategy with session storage
170
- def current_organization
171
- @current_organization ||=
172
- current_user.organizations.find_by(id: session[:organization_id]) ||
173
- current_user.organizations.first
118
+ ### 1. Direct `belongs_to`
119
+
120
+ ```ruby
121
+ class Post < ResourceRecord
122
+ belongs_to :organization
174
123
  end
124
+ # Post.associated_with(org) → Post.where(organization: org)
175
125
  ```
176
126
 
177
- ### Organization Switcher
127
+ Auto-detected. Use when the model naturally has a direct FK to the entity.
128
+
129
+ ### 2. Join table (`belongs_to` AND `belongs_to`)
178
130
 
179
131
  ```ruby
180
- class OrganizationSwitchController < ApplicationController
181
- def update
182
- org = current_user.organizations.find(params[:id])
183
- session[:organization_id] = org.id
184
- redirect_back(fallback_location: root_path)
185
- end
132
+ class Membership < ResourceRecord
133
+ belongs_to :user
134
+ belongs_to :organization # auto-detected
186
135
  end
187
136
  ```
188
137
 
189
- ## Policy Integration
190
-
191
- Entity scoping is automatic. The base `Plutonium::Resource::Policy` includes:
138
+ ### 3. Grandchild — `has_one :through`
192
139
 
193
140
  ```ruby
194
- relation_scope do |relation|
195
- next relation unless entity_scope
196
-
197
- relation.associated_with(entity_scope)
141
+ class Post < ResourceRecord
142
+ belongs_to :user
143
+ has_one :organization, through: :user # ← critical
198
144
  end
199
145
  ```
200
146
 
201
- The `entity_scope` context is automatically set to `current_scoped_entity`.
147
+ Auto-detected via `reflect_on_all_associations`. Declaring `has_one :through` is the lightest fix when the path is two hops.
202
148
 
203
- ### Additional Filtering
149
+ Full mechanics: [Reference › Tenancy › Entity scoping › Three model shapes](/reference/tenancy/entity-scoping#three-model-shapes).
204
150
 
205
- Add role-based filtering on top of entity scoping:
151
+ ## Custom scope (when the path is polymorphic or needs SQL control)
206
152
 
207
153
  ```ruby
208
- class PostPolicy < ResourcePolicy
209
- relation_scope do |relation|
210
- relation = super(relation) # Apply entity scoping first
211
-
212
- if user.role == "viewer"
213
- relation.where(published: true)
214
- else
215
- relation
216
- end
217
- end
154
+ class Comment < ResourceRecord
155
+ scope :associated_with_organization, ->(org) {
156
+ joins(task: :project).where(projects: {organization_id: org.id})
157
+ }
218
158
  end
219
159
  ```
220
160
 
221
- ## Subdomain-Based Tenancy
222
-
223
- Route to different organizations by subdomain:
161
+ Plutonium picks this up **before** trying association detection.
224
162
 
225
- ### Routes
163
+ ## Accessing the scoped entity
226
164
 
227
165
  ```ruby
228
- # config/routes.rb
229
- constraints subdomain: /[a-z]+/ do
230
- mount CustomerPortal::Engine, at: "/"
231
- end
166
+ # Controller / views
167
+ current_scoped_entity
168
+ scoped_to_entity?
169
+
170
+ # Policy
171
+ entity_scope
232
172
  ```
233
173
 
234
- ### Custom Strategy
174
+ ## Policy filtering on top of default
235
175
 
236
176
  ```ruby
237
- # Engine configuration
238
- scope_to_entity Organization, strategy: :current_organization
239
-
240
- # Controller concern
241
- def current_organization
242
- @current_organization ||=
243
- Organization.find_by!(subdomain: request.subdomain)
177
+ relation_scope do |relation|
178
+ default_relation_scope(relation).where(archived: false)
244
179
  end
245
180
  ```
246
181
 
247
- ## Cross-Tenant Operations
182
+ 🚨 `default_relation_scope(relation)` must be called explicitly — not `super`. Bypassing it raises at runtime.
248
183
 
249
- Sometimes admins need to see all data:
184
+ ## Cross-tenant operations super-admin portal
250
185
 
251
- ### Super Admin Portal (No Scoping)
186
+ Create a separate portal **without** `scope_to_entity`:
252
187
 
253
188
  ```ruby
254
- # packages/super_admin_portal/lib/engine.rb
255
189
  module SuperAdminPortal
256
190
  class Engine < Rails::Engine
257
191
  include Plutonium::Portal::Engine
258
- # No scope_to_entity = sees everything
192
+ # No scope_to_entity sees all tenants
259
193
  end
260
194
  end
261
195
  ```
262
196
 
263
- ### Conditional Scoping
197
+ This portal's policies see everything. Don't enable public signup here.
264
198
 
265
- ```ruby
266
- # Custom strategy that returns nil for super admins
267
- def current_organization
268
- return nil if current_user.super_admin?
269
- current_user.organization
270
- end
271
- ```
272
-
273
- When `current_scoped_entity` returns `nil`, scoping is bypassed.
199
+ ## Multiple associations to the same entity
274
200
 
275
- ## Data Isolation Patterns
201
+ If a model has two `belongs_to` to the entity class (e.g. `Match belongs_to :home_team, :away_team`), Plutonium raises:
276
202
 
277
- ### Shared Database, Scoped Queries (Recommended)
203
+ ```
204
+ Match has multiple associations to Competition::Team: home_team, away_team.
205
+ Plutonium cannot auto-detect which one to use for entity scoping.
206
+ ```
278
207
 
279
- All tenants share tables, queries filter by entity association:
208
+ Override on the controller:
280
209
 
281
210
  ```ruby
282
- scope_to_entity Organization, strategy: :path
211
+ class MatchesController < ::ResourceController
212
+ private
213
+ def scoped_entity_association = :home_team
214
+ end
283
215
  ```
284
216
 
285
- Pros:
286
- - Simple setup
287
- - Easy migrations
288
- - Efficient for many small tenants
289
-
290
- Cons:
291
- - Risk of data leakage if scoping fails
292
- - Complex queries for cross-tenant reports
293
-
294
- ### Schema-Based Isolation
217
+ ## Common issues
295
218
 
296
- Each tenant has separate database schema. This requires additional setup beyond Plutonium's built-in scoping.
219
+ - **`verify_default_relation_scope_applied!` raises** your custom `relation_scope` doesn't call `default_relation_scope(relation)`. Fix by composing: `default_relation_scope(relation).where(...)`.
220
+ - **`Could not resolve the association between 'Model' and 'Entity'`** — the model has no path to the entity. Fix on the **model** (declare `has_one :through` or a custom `associated_with_<entity>` scope). Never paper over with `where` in the policy.
221
+ - **Records leak across tenants** — likely a missing compound-uniqueness scope on the model. Add `validates :code, uniqueness: {scope: :organization_id}`.
222
+ - **Forms show the entity field anyway** — check `present_scoped_entity?` / `submit_scoped_entity?` on the controller (defaults are `false`).
223
+ - **Want to bypass scoping in one place** — use `skip_default_relation_scope!` explicitly, NOT a silent `where` bypass.
297
224
 
298
225
  ## Related
299
226
 
300
- - [Authorization](./authorization)
301
- - [Creating Packages](./creating-packages)
302
- - [Authentication](./authentication)
227
+ - [Reference › Tenancy › Entity scoping](/reference/tenancy/entity-scoping) — full surface
228
+ - [Reference › Behavior › Policies](/reference/behavior/policies) — `relation_scope` syntax
229
+ - [Reference › App › Portals](/reference/app/portals) — `scope_to_entity` engine config
230
+ - [Nested resources](./nested-resources) — parent scoping (takes precedence over entity scoping)
231
+ - [User invites](./user-invites) — invitation-based membership onboarding