plutonium 0.50.0 → 0.52.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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +574 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +167 -302
  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 +674 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +9 -6
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +44 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1010 -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 +38 -29
  18. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  19. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  20. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  21. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  22. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  23. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  24. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  25. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  26. data/docs/.vitepress/theme/custom.css +144 -0
  27. data/docs/.vitepress/theme/index.ts +58 -1
  28. data/docs/getting-started/index.md +33 -57
  29. data/docs/getting-started/installation.md +37 -80
  30. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  31. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  32. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  33. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  34. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  35. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  36. data/docs/getting-started/tutorial/index.md +4 -5
  37. data/docs/guides/adding-resources.md +66 -377
  38. data/docs/guides/authentication.md +98 -462
  39. data/docs/guides/authorization.md +124 -370
  40. data/docs/guides/creating-packages.md +93 -298
  41. data/docs/guides/custom-actions.md +126 -441
  42. data/docs/guides/customizing-ui.md +258 -0
  43. data/docs/guides/index.md +49 -52
  44. data/docs/guides/multi-tenancy.md +123 -186
  45. data/docs/guides/nested-resources.md +137 -396
  46. data/docs/guides/search-filtering.md +127 -238
  47. data/docs/guides/testing.md +10 -5
  48. data/docs/guides/theming.md +168 -405
  49. data/docs/guides/troubleshooting.md +5 -3
  50. data/docs/guides/user-invites.md +112 -425
  51. data/docs/guides/user-profile.md +82 -241
  52. data/docs/index.md +10 -219
  53. data/docs/public/asciinema/home-scaffold.cast +305 -0
  54. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  55. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  56. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  57. data/docs/public/images/guides/nested-inputs.png +0 -0
  58. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  59. data/docs/public/images/guides/search-filtering-index.png +0 -0
  60. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  61. data/docs/public/images/guides/theming-after.png +0 -0
  62. data/docs/public/images/guides/theming-before.png +0 -0
  63. data/docs/public/images/guides/user-invites-landing.png +0 -0
  64. data/docs/public/images/guides/user-profile-edit.png +0 -0
  65. data/docs/public/images/guides/user-profile-show.png +0 -0
  66. data/docs/public/images/home-index.png +0 -0
  67. data/docs/public/images/home-new.png +0 -0
  68. data/docs/public/images/home-show.png +0 -0
  69. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  70. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  71. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  72. data/docs/public/images/tutorial/02-new-form.png +0 -0
  73. data/docs/public/images/tutorial/03-create-account.png +0 -0
  74. data/docs/public/images/tutorial/03-login.png +0 -0
  75. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  76. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  77. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  78. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  79. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  80. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  81. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  82. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  83. data/docs/reference/app/generators.md +517 -0
  84. data/docs/reference/app/index.md +158 -0
  85. data/docs/reference/app/packages.md +146 -0
  86. data/docs/reference/app/portals.md +377 -0
  87. data/docs/reference/auth/accounts.md +229 -0
  88. data/docs/reference/auth/index.md +88 -0
  89. data/docs/reference/auth/profile.md +185 -0
  90. data/docs/reference/behavior/controllers.md +395 -0
  91. data/docs/reference/behavior/index.md +22 -0
  92. data/docs/reference/behavior/interactions.md +341 -0
  93. data/docs/reference/behavior/policies.md +417 -0
  94. data/docs/reference/index.md +67 -48
  95. data/docs/reference/resource/actions.md +423 -0
  96. data/docs/reference/resource/definition.md +508 -0
  97. data/docs/reference/resource/index.md +50 -0
  98. data/docs/reference/resource/model.md +348 -0
  99. data/docs/reference/resource/query.md +305 -0
  100. data/docs/reference/tenancy/entity-scoping.md +368 -0
  101. data/docs/reference/tenancy/index.md +36 -0
  102. data/docs/reference/tenancy/invites.md +400 -0
  103. data/docs/reference/tenancy/nested-resources.md +267 -0
  104. data/docs/reference/testing/index.md +287 -0
  105. data/docs/reference/ui/assets.md +400 -0
  106. data/docs/reference/ui/components.md +165 -0
  107. data/docs/reference/ui/displays.md +104 -0
  108. data/docs/reference/ui/forms.md +284 -0
  109. data/docs/reference/ui/index.md +30 -0
  110. data/docs/reference/ui/layouts.md +106 -0
  111. data/docs/reference/ui/pages.md +189 -0
  112. data/docs/reference/ui/tables.md +121 -0
  113. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  114. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  115. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  116. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  117. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  118. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  119. data/gemfiles/rails_7.gemfile.lock +1 -1
  120. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  121. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  122. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  123. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  124. data/lib/generators/pu/invites/install_generator.rb +45 -0
  125. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  126. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  127. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  128. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  129. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  130. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  131. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  132. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  133. data/lib/generators/pu/saas/membership/USAGE +4 -1
  134. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  135. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  136. data/lib/plutonium/definition/base.rb +1 -1
  137. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  138. data/lib/plutonium/helpers/turbo_helper.rb +30 -0
  139. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  140. data/lib/plutonium/resource/controller.rb +1 -0
  141. data/lib/plutonium/resource/controllers/crud_actions.rb +23 -5
  142. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  143. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  144. data/lib/plutonium/resource/policy.rb +7 -0
  145. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  146. data/lib/plutonium/ui/component/methods.rb +5 -0
  147. data/lib/plutonium/ui/form/base.rb +23 -3
  148. data/lib/plutonium/ui/form/components/json.rb +58 -0
  149. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  150. data/lib/plutonium/ui/form/components/secure_association.rb +103 -22
  151. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  152. data/lib/plutonium/ui/form/interaction.rb +1 -1
  153. data/lib/plutonium/ui/form/resource.rb +0 -4
  154. data/lib/plutonium/ui/form/theme.rb +1 -1
  155. data/lib/plutonium/ui/grid/resource.rb +1 -1
  156. data/lib/plutonium/ui/layout/base.rb +1 -0
  157. data/lib/plutonium/ui/page/base.rb +0 -7
  158. data/lib/plutonium/ui/page/edit.rb +1 -1
  159. data/lib/plutonium/ui/page/index.rb +4 -4
  160. data/lib/plutonium/ui/page/new.rb +1 -1
  161. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  162. data/lib/plutonium/ui/table/resource.rb +1 -1
  163. data/lib/plutonium/version.rb +1 -1
  164. data/lib/plutonium.rb +8 -0
  165. data/lib/tasks/release.rake +15 -1
  166. data/package.json +13 -10
  167. data/src/css/slim_select.css +4 -0
  168. data/src/js/controllers/form_controller.js +5 -4
  169. data/src/js/controllers/slim_select_controller.js +61 -0
  170. data/src/js/turbo/turbo_actions.js +33 -0
  171. data/yarn.lock +661 -544
  172. metadata +86 -33
  173. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  174. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  175. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  176. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  177. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  178. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  179. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  180. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  181. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  182. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  183. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  184. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  185. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  186. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  187. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  188. data/docs/reference/assets/index.md +0 -496
  189. data/docs/reference/controller/index.md +0 -412
  190. data/docs/reference/definition/actions.md +0 -462
  191. data/docs/reference/definition/fields.md +0 -383
  192. data/docs/reference/definition/index.md +0 -326
  193. data/docs/reference/definition/query.md +0 -351
  194. data/docs/reference/generators/index.md +0 -648
  195. data/docs/reference/interaction/index.md +0 -449
  196. data/docs/reference/model/features.md +0 -248
  197. data/docs/reference/model/index.md +0 -218
  198. data/docs/reference/policy/index.md +0 -456
  199. data/docs/reference/portal/index.md +0 -379
  200. data/docs/reference/views/forms.md +0 -411
  201. data/docs/reference/views/index.md +0 -544
@@ -1,381 +1,176 @@
1
1
  # Creating Packages
2
2
 
3
- This guide covers creating and organizing Feature Packages and Portal Packages.
3
+ Organize your app into feature and portal packages.
4
4
 
5
- ## Package Types
5
+ ## Goal
6
6
 
7
- | Type | Purpose | Generator |
8
- |------|---------|-----------|
9
- | **Feature Package** | Business logic (models, definitions, policies) | `rails g pu:pkg:package NAME` |
10
- | **Portal Package** | Web interface (routes, auth, UI) | `rails g pu:pkg:portal NAME` |
7
+ Domain code (models, policies, definitions, interactions) lives in **feature packages**. Web interfaces (controllers, views, routes, auth) live in **portal packages**. Both are Rails engines with Plutonium conventions on top.
11
8
 
12
- ## Creating a Feature Package
9
+ ## Two types
13
10
 
14
- ### Using the Generator
11
+ | Type | Purpose | Generator | Examples |
12
+ |---|---|---|---|
13
+ | **Feature** | Business logic | `pu:pkg:package NAME` | `blogging`, `billing`, `inventory` |
14
+ | **Portal** | Web interface | `pu:pkg:portal NAME` | `admin_portal`, `customer_portal`, `public_portal` |
15
+
16
+ 🚨 Don't mix the two. Feature packages own the **domain code** — models, interactions, policies/definitions for resources owned by that feature. Portal packages own the **web surface** — controllers, routes, auth, and portal-specific policy/definition *overrides* for resources they expose.
17
+
18
+ ## Feature package
19
+
20
+ ### 1. Generate
15
21
 
16
22
  ```bash
17
23
  rails g pu:pkg:package blogging
18
24
  ```
19
25
 
20
- ### Generated Structure
26
+ ### 2. Structure
21
27
 
22
28
  ```
23
29
  packages/blogging/
24
30
  ├── app/
25
- │ ├── controllers/blogging/
26
- │ └── resource_controller.rb
27
- │ ├── definitions/blogging/
28
- └── resource_definition.rb
29
- ├── interactions/blogging/
30
- │ │ └── resource_interaction.rb
31
- │ ├── models/blogging/
32
- │ │ └── resource_record.rb
33
- │ ├── policies/blogging/
34
- │ │ └── resource_policy.rb
35
- │ └── views/blogging/
36
- └── lib/
37
- └── engine.rb
31
+ │ ├── models/blogging/ # Blogging::Post
32
+ ├── definitions/blogging/ # Blogging::PostDefinition
33
+ │ ├── policies/blogging/ # Blogging::PostPolicy
34
+ │ └── interactions/blogging/ # Blogging::PublishPostInteraction
35
+ ├── db/migrate/
36
+ └── lib/engine.rb
38
37
  ```
39
38
 
40
- ### Engine Configuration
41
-
42
- ```ruby
43
- # packages/blogging/lib/engine.rb
44
- module Blogging
45
- class Engine < Rails::Engine
46
- include Plutonium::Package::Engine
47
- end
48
- end
49
- ```
50
-
51
- ### Namespacing
52
-
53
- All classes are auto-namespaced:
54
- - `app/models/blogging/post.rb` → `Blogging::Post`
55
- - `app/policies/blogging/post_policy.rb` → `Blogging::PostPolicy`
56
-
57
- ## Creating a Portal Package
58
-
59
- ### Using the Generator
39
+ ### 3. Create resources inside it
60
40
 
61
41
  ```bash
62
- rails g pu:pkg:portal admin
42
+ rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
43
+ rails db:migrate
63
44
  ```
64
45
 
65
- ### Generator Options
66
-
67
- | Option | Description |
68
- |--------|-------------|
69
- | `--auth=NAME` | Rodauth account to authenticate with |
70
- | `--public` | Grant public access (no authentication) |
71
- | `--byo` | Bring your own authentication |
46
+ ### 4. Expose it via a portal
72
47
 
73
48
  ```bash
74
- # Non-interactive examples
75
- rails g pu:pkg:portal admin --auth=admin
76
- rails g pu:pkg:portal api --public
77
- rails g pu:pkg:portal custom --byo
49
+ rails g pu:res:conn Blogging::Post --dest=admin_portal
78
50
  ```
79
51
 
80
- Without flags, the generator prompts interactively.
81
-
82
- ### Generated Structure
83
-
84
- ```
85
- packages/admin_portal/
86
- ├── app/
87
- │ ├── controllers/admin_portal/
88
- │ │ ├── concerns/controller.rb
89
- │ │ ├── dashboard_controller.rb
90
- │ │ ├── plutonium_controller.rb
91
- │ │ └── resource_controller.rb
92
- │ ├── definitions/admin_portal/
93
- │ │ └── resource_definition.rb
94
- │ ├── policies/admin_portal/
95
- │ │ └── resource_policy.rb
96
- │ └── views/admin_portal/
97
- │ └── dashboard/index.html.erb
98
- ├── config/
99
- │ └── routes.rb
100
- └── lib/
101
- └── engine.rb
102
- ```
52
+ ## Portal package
103
53
 
104
- ### Portal Engine
54
+ ### 1. Generate
105
55
 
106
- ```ruby
107
- # packages/admin_portal/lib/engine.rb
108
- module AdminPortal
109
- class Engine < Rails::Engine
110
- include Plutonium::Portal::Engine
111
-
112
- config.after_initialize do
113
- # Multi-tenancy (optional)
114
- scope_to_entity Organization, strategy: :path
115
- end
116
- end
117
- end
56
+ ```bash
57
+ rails g pu:pkg:portal admin --auth=user
118
58
  ```
119
59
 
120
- ### Portal Authentication
60
+ Options:
121
61
 
122
- Authentication is configured in the controller concern based on generator options:
62
+ - `--auth=NAME` Rodauth account to authenticate with.
63
+ - `--public` — public access, no auth.
64
+ - `--byo` — bring your own auth.
65
+ - `--scope=CLASS` — entity class for multi-tenancy.
123
66
 
124
- ```ruby
125
- # packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
126
- module AdminPortal
127
- module Concerns
128
- module Controller
129
- extend ActiveSupport::Concern
130
- include Plutonium::Portal::Controller
131
- include Plutonium::Auth::Rodauth(:admin)
132
- end
133
- end
134
- end
135
- ```
67
+ The generator mounts the engine for you — at `/admin` in this case, wrapped in `constraints Rodauth::Rails.authenticate(:user)` because you passed `--auth=user`. Open `packages/admin_portal/config/routes.rb` to see the generated mount.
136
68
 
137
- For public access:
69
+ ### 2. Connect resources
138
70
 
139
- ```ruby
140
- include Plutonium::Auth::Public
71
+ ```bash
72
+ rails g pu:res:conn Blogging::Post --dest=admin_portal
141
73
  ```
142
74
 
143
- For custom authentication:
75
+ You can connect multiple resources in one command:
144
76
 
145
- ```ruby
146
- included do
147
- helper_method :current_user
148
- end
149
-
150
- def current_user
151
- # Your authentication logic
152
- @current_user ||= User.find_by(api_key: request.headers["X-API-Key"])
153
- end
77
+ ```bash
78
+ rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
154
79
  ```
155
80
 
156
- ## Portal Routes
157
-
158
- The portal generator creates routes and auto-mounts to the main app:
81
+ See [Reference › App › Portals](/reference/app/portals) for the full portal surface.
159
82
 
160
- ```ruby
161
- # packages/admin_portal/config/routes.rb
162
- AdminPortal::Engine.routes.draw do
163
- root to: "dashboard#index"
83
+ ## Auto-namespacing
164
84
 
165
- # Register resources here
166
- register_resource ::Post
167
- register_resource Blogging::Comment
168
- end
85
+ Every file under `app/<kind>/blogging/` resolves to `Blogging::*`:
169
86
 
170
- # Also adds to main app routes:
171
- # config/routes.rb (auto-generated)
172
- Rails.application.routes.draw do
173
- constraints Rodauth::Rails.authenticate(:admin) do
174
- mount AdminPortal::Engine, at: "/admin"
175
- end
176
- end
177
- ```
87
+ - `app/models/blogging/post.rb` `Blogging::Post`
88
+ - `app/policies/blogging/post_policy.rb` → `Blogging::PostPolicy`
178
89
 
179
- ### Custom Routes on Resources
90
+ Each feature package gets base classes — `Blogging::ApplicationRecord`, `Blogging::ResourceRecord`, `Blogging::ResourcePolicy`, `Blogging::ResourceDefinition`, `Blogging::ResourceInteraction` — that inherit from the main app's.
180
91
 
181
- Add member or collection routes with a block:
92
+ ## Cross-package references
182
93
 
183
- ```ruby
184
- register_resource ::Post do
185
- member do
186
- get :preview
187
- post :publish
188
- end
189
- collection do
190
- get :archived
191
- end
192
- end
94
+ ```bash
95
+ rails g pu:res:scaffold Comment user:belongs_to blogging/post:belongs_to body:text --dest=comments
193
96
  ```
194
97
 
195
- ## Package Loading
98
+ The `blogging/post` syntax expands to `Blogging::Post`.
196
99
 
197
- Packages are loaded via `config/packages.rb` (generated during install):
100
+ ## When to use which
198
101
 
199
- ```ruby
200
- # config/packages.rb
201
- Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
202
- load package
203
- end
204
- ```
205
-
206
- This is automatically required in `config/application.rb`.
102
+ ### Feature package
207
103
 
208
- ## Adding Resources to Packages
104
+ When the code:
209
105
 
210
- ```bash
211
- # Add to main app
212
- rails g pu:res:scaffold Post title:string --dest=main_app
106
+ - Could be reused across multiple portals (admin and customer both edit `Blogging::Post`).
107
+ - Has no inherent UI / auth.
108
+ - You want isolated from other domains (`billing` shouldn't depend on `blogging`).
213
109
 
214
- # Add to a feature package
215
- rails g pu:res:scaffold Post title:string --dest=blogging
216
- ```
110
+ ### Portal package
217
111
 
218
- Resources are namespaced:
112
+ When the code:
219
113
 
220
- ```ruby
221
- # packages/blogging/app/models/blogging/post.rb
222
- module Blogging
223
- class Post < Blogging::ResourceRecord
224
- # Model code
225
- end
226
- end
227
- ```
114
+ - Has a specific auth flow (admin vs customer vs public).
115
+ - Renders different views of the same underlying resources.
116
+ - Needs different policies / definitions per audience.
228
117
 
229
- ## Connecting Resources to Portals
118
+ ### When NOT to make a package
230
119
 
231
- Resources must be connected to portals to be accessible:
120
+ For an app that doesn't need cross-portal sharing, just put resources in `--dest=main_app`. Packages add organization, not power.
232
121
 
233
- ```bash
234
- # Connect main app resource
235
- rails g pu:res:conn Post --dest=admin_portal
122
+ ## Typical architecture
236
123
 
237
- # Connect namespaced resource
238
- rails g pu:res:conn Blogging::Post --dest=admin_portal
239
124
  ```
240
-
241
- ## Entity Scoping (Multi-tenancy)
242
-
243
- Automatically scope all data to a parent entity:
244
-
245
- ### Path Strategy
246
-
247
- Entity ID in URL path:
248
-
249
- ```ruby
250
- # packages/admin_portal/lib/engine.rb
251
- config.after_initialize do
252
- scope_to_entity Organization, strategy: :path
253
- end
125
+ packages/
126
+ ├── blogging/ # Feature: blog functionality
127
+ ├── billing/ # Feature: payments/invoicing
128
+ ├── admin_portal/ # Portal: admin interface
129
+ └── customer_portal/ # Portal: customer dashboard
254
130
  ```
255
131
 
256
- Routes become: `/organizations/:organization_id/posts`
132
+ The portals expose the features. A single feature can be exposed by multiple portals — usually with different policies and definitions per portal.
257
133
 
258
- ### Custom Strategy
134
+ ## Package loading
259
135
 
260
- Implement your own lookup method:
136
+ Generated by `pu:core:install`:
261
137
 
262
138
  ```ruby
263
- config.after_initialize do
264
- scope_to_entity Organization, strategy: :current_organization
265
- end
266
-
267
- # In controller concern
268
- def current_organization
269
- @current_organization ||= Organization.find_by!(subdomain: request.subdomain)
139
+ # config/packages.rb
140
+ Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
141
+ load package
270
142
  end
271
143
  ```
272
144
 
273
- ## Package Best Practices
274
-
275
- ### 1. Single Responsibility
276
- Each feature package should handle one domain:
277
- - `blogging` - Posts, comments, categories
278
- - `inventory` - Products, stock, warehouses
279
- - `billing` - Invoices, payments, subscriptions
280
-
281
- ### 2. Clear Naming
282
- - Feature packages: domain nouns (`blogging`, `billing`)
283
- - Portal packages: role + portal (`admin_portal`, `api_portal`)
284
-
285
- ### 3. Minimal Cross-Dependencies
286
- Limit dependencies between feature packages. If two packages are tightly coupled, consider merging them.
287
-
288
- ### 4. Portal Customization
289
- Put UI customizations in portal packages, not feature packages:
290
-
291
- ```ruby
292
- # Good: Portal-specific definition
293
- # packages/admin_portal/app/definitions/admin_portal/post_definition.rb
294
-
295
- # Bad: Feature package with portal-specific code
296
- # packages/blogging/app/definitions/blogging/admin_post_definition.rb
297
- ```
298
-
299
- ## Multiple Portals Pattern
300
-
301
- Common pattern for different user types:
302
-
303
- ```
304
- packages/
305
- ├── blogging/ # Feature: blog functionality
306
- ├── billing/ # Feature: payment/invoicing
307
- ├── admin_portal/ # Portal: admin interface
308
- ├── dashboard_portal/ # Portal: user dashboard
309
- └── public_portal/ # Portal: public read-only
310
- ```
145
+ Loaded from `config/application.rb`. Migrations from all packages are picked up by `rails db:migrate` automatically.
311
146
 
312
- Each portal can:
313
- - Have different authentication
314
- - Show different fields
315
- - Allow different actions
316
- - Use different layouts
317
-
318
- ## Portal-Specific Overrides
319
-
320
- ### Override Definition
147
+ ## Per-portal overrides
321
148
 
322
149
  ```ruby
323
- # packages/admin_portal/app/definitions/admin_portal/post_definition.rb
150
+ # Definition — different fields per portal
324
151
  class AdminPortal::PostDefinition < ::PostDefinition
325
- # Add portal-specific scopes
326
- scope :my_posts, -> { where(user: current_user) }
152
+ input :internal_notes, as: :text # admins see this; customers don't
153
+ scope :pending_review
327
154
  end
328
- ```
329
155
 
330
- ### Override Policy
331
-
332
- ```ruby
333
- # packages/admin_portal/app/policies/admin_portal/post_policy.rb
156
+ # Policy — different rules per portal
334
157
  class AdminPortal::PostPolicy < ::PostPolicy
335
158
  include AdminPortal::ResourcePolicy
336
159
 
337
- def destroy?
338
- true # Admins can delete
339
- end
340
-
341
- def permitted_attributes_for_create
342
- %i[title content featured internal_notes] # More fields
343
- end
160
+ def destroy? = true
161
+ def permitted_attributes_for_create = %i[title content featured internal_notes]
344
162
  end
345
163
  ```
346
164
 
347
- ### Override Controller
165
+ ## Common issues
348
166
 
349
- ```ruby
350
- # packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
351
- class AdminPortal::PostsController < ::PostsController
352
- include AdminPortal::Concerns::Controller
353
-
354
- private
355
-
356
- def preferred_action_after_submit
357
- "index"
358
- end
359
- end
360
- ```
361
-
362
- ## Controller Hierarchy
363
-
364
- Portal controllers inherit from the feature package's controller if one exists (and include the portal's `Concerns::Controller`). If no feature package controller exists, they inherit from the portal's `ResourceController`.
365
-
366
- ```ruby
367
- # With feature package controller:
368
- class AdminPortal::PostsController < ::PostsController
369
- include AdminPortal::Concerns::Controller
370
- end
371
-
372
- # Without feature package controller:
373
- class AdminPortal::PostsController < AdminPortal::ResourceController
374
- end
375
- ```
167
+ - **Class not loading** — namespace must match the directory: `app/models/blogging/post.rb` MUST be `Blogging::Post`.
168
+ - **Migration not running** — package migrations are auto-included. If they aren't running, check `config/packages.rb` is loaded from `application.rb`.
169
+ - **Cross-package association fails** — use `blogging/post:belongs_to` in `pu:res:scaffold`, OR manually set `class_name: "Blogging::Post"` on the `belongs_to`.
376
170
 
377
171
  ## Related
378
172
 
379
- - [Adding Resources](./adding-resources)
380
- - [Authentication](./authentication)
381
- - [Multi-tenancy](./multi-tenancy)
173
+ - [Reference › App › Packages](/reference/app/packages) — full package surface
174
+ - [Reference › App › Portals](/reference/app/portals) — portal-specific configuration
175
+ - [Adding resources](./adding-resources) — `pu:res:scaffold` and `pu:res:conn`
176
+ - [Authentication](./authentication) — portal auth setup