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
@@ -0,0 +1,417 @@
1
+ # Policy
2
+
3
+ Authorization for resources. Built on [ActionPolicy](https://actionpolicy.evilmartians.io/). Plutonium adds:
4
+
5
+ - Attribute permissions (`permitted_attributes_for_*`)
6
+ - Association permissions (`permitted_associations`)
7
+ - Automatic entity scoping via `default_relation_scope`
8
+ - Derived action methods (`update?` inherits from `create?`, etc.)
9
+
10
+ ## 🚨 Critical
11
+
12
+ - **`create?` and `read?` default to `false`.** You MUST override them explicitly. Everything else (`update?`, `destroy?`, `index?`, `show?`, …) derives from one of those.
13
+ - **`permitted_attributes_for_*` must be explicit in production.** Dev auto-detects; production raises.
14
+ - **`relation_scope` must call `default_relation_scope(relation)` explicitly** — never `super`. Bypassing it triggers `verify_default_relation_scope_applied!`.
15
+ - **For `has_cents` fields, use the virtual name** (`:price`), NEVER `:price_cents`.
16
+ - **Don't put `*_attributes` hashes in `permitted_attributes_for_*`.** Nested forms are extracted from the form definition, not the policy. List the association name (`:variants`) and the `nested_input` in the definition handles the rest.
17
+ - **Custom action ⇒ policy method.** `action :publish` needs `def publish?`. Undefined methods return `false` → action silently disappears.
18
+ - **Index has no `record`.** Record-dependent `_for_read` overrides need an explicit `_for_index` too (see [below](#index-has-no-record)).
19
+
20
+ ## Base class
21
+
22
+ ```ruby
23
+ # app/policies/resource_policy.rb — installed once
24
+ class ResourcePolicy < Plutonium::Resource::Policy
25
+ end
26
+
27
+ # app/policies/post_policy.rb — per resource, generated
28
+ class PostPolicy < ResourcePolicy
29
+ def create? = user.present?
30
+ def read? = true
31
+
32
+ def permitted_attributes_for_create
33
+ %i[title content]
34
+ end
35
+
36
+ def permitted_attributes_for_read
37
+ %i[title content author created_at]
38
+ end
39
+ end
40
+ ```
41
+
42
+ ## Authorization context
43
+
44
+ Inside a policy:
45
+
46
+ | Variable | Description |
47
+ |---|---|
48
+ | `user` | Current authenticated user (required) |
49
+ | `record` | Resource being authorized |
50
+ | `entity_scope` | Current scoped entity (multi-tenancy) |
51
+ | `parent` | Parent record for nested resources (nil otherwise) |
52
+ | `parent_association` | Association name on parent (e.g. `:comments`) |
53
+
54
+ ## Action permissions
55
+
56
+ ### Must override
57
+
58
+ ```ruby
59
+ def create? # default: false
60
+ user.present?
61
+ end
62
+
63
+ def read? # default: false
64
+ true
65
+ end
66
+ ```
67
+
68
+ ### Derived (inherit automatically)
69
+
70
+ | Method | Inherits from | Override when |
71
+ |---|---|---|
72
+ | `update?` | `create?` | Different update rules |
73
+ | `destroy?` | `create?` | Different delete rules |
74
+ | `index?` | `read?` | Custom listing rules |
75
+ | `show?` | `read?` | Record-specific read rules |
76
+ | `new?` | `create?` | Rarely needed |
77
+ | `edit?` | `update?` | Rarely needed |
78
+ | `search?` | `index?` | Search-specific rules |
79
+ | `typeahead?` | `index?` | Autocomplete on inputs/filters targeting this resource |
80
+
81
+ ### Custom actions
82
+
83
+ Define `def <action>?` matching the definition's `action :<action>`. Undefined methods return `false`:
84
+
85
+ ```ruby
86
+ def publish? = update? && record.draft?
87
+ def archive? = create? && !record.archived?
88
+ def invite_user? = user.admin?
89
+ ```
90
+
91
+ ### Bulk actions — per-record authorization
92
+
93
+ ```ruby
94
+ def bulk_archive?
95
+ create? && !record.locked? # checked per record in the selection
96
+ end
97
+ ```
98
+
99
+ How it works:
100
+
101
+ - Policy is checked **per record** in the selected set.
102
+ - **Backend:** if any record fails, the entire request is rejected.
103
+ - **UI:** only actions ALL selected records support are shown (intersection).
104
+ - Records come from `current_authorized_scope` — users can only select records they're allowed to access.
105
+
106
+ ## Attribute permissions
107
+
108
+ ```ruby
109
+ # Must override for production
110
+ def permitted_attributes_for_read
111
+ %i[title content author published_at created_at]
112
+ end
113
+
114
+ def permitted_attributes_for_create
115
+ %i[title content]
116
+ end
117
+ ```
118
+
119
+ ### Derived
120
+
121
+ | Method | Inherits from |
122
+ |---|---|
123
+ | `permitted_attributes_for_update` | `permitted_attributes_for_create` |
124
+ | `permitted_attributes_for_index` | `permitted_attributes_for_read` |
125
+ | `permitted_attributes_for_show` | `permitted_attributes_for_read` |
126
+ | `permitted_attributes_for_new` | `permitted_attributes_for_create` |
127
+ | `permitted_attributes_for_edit` | `permitted_attributes_for_update` |
128
+
129
+ ### Per-action override
130
+
131
+ ```ruby
132
+ def permitted_attributes_for_index
133
+ %i[title author created_at] # minimal for the table
134
+ end
135
+
136
+ def permitted_attributes_for_read
137
+ %i[title content author tags created_at] # fuller for the show page
138
+ end
139
+ ```
140
+
141
+ ### Index has no `record`
142
+
143
+ 🚨 `permitted_attributes_for_index` is evaluated at the **collection level** — `record` is `nil`. `permitted_attributes_for_show` (and `_for_read`) ARE evaluated per record.
144
+
145
+ If you write a record-dependent `_for_read`:
146
+
147
+ ```ruby
148
+ def permitted_attributes_for_read
149
+ attrs = %i[title content]
150
+ attrs << :archive_reason if record.archived? # uses record
151
+ attrs
152
+ end
153
+ ```
154
+
155
+ …you MUST also define an explicit `permitted_attributes_for_index` — otherwise inheritance kicks in, runs the `_for_read` body during the table render, and `record.archived?` blows up on `NoMethodError: undefined method 'archived?' for nil`.
156
+
157
+ ```ruby
158
+ def permitted_attributes_for_index
159
+ %i[title content] # no record-dependent fields
160
+ end
161
+ ```
162
+
163
+ Same rule for `permitted_attributes_for_create` vs `_for_new` (new has no persisted record).
164
+
165
+ ### Conditional attribute access
166
+
167
+ ```ruby
168
+ def permitted_attributes_for_create
169
+ attrs = %i[title content]
170
+ attrs += %i[featured author_id] if user.admin?
171
+ attrs
172
+ end
173
+
174
+ def permitted_attributes_for_update
175
+ case record.status
176
+ when 'draft' then %i[title content category_id]
177
+ when 'published' then %i[content] # only the body once published
178
+ else []
179
+ end
180
+ end
181
+ ```
182
+
183
+ ### Definition declares HOW, policy declares WHAT
184
+
185
+ `permitted_attributes_for_*` controls **which fields appear** on a view. The definition's `field`/`input`/`display`/`column` declarations only control **how** they render. A `field :name` in the definition does nothing unless `:name` is also in the relevant `permitted_attributes_for_*`.
186
+
187
+ Common mistake: adding a definition declaration and wondering why the field doesn't show — check the policy.
188
+
189
+ ### Anti-pattern: nested-attributes hashes
190
+
191
+ ```ruby
192
+ # ❌ NEVER
193
+ def permitted_attributes_for_create
194
+ [
195
+ :name,
196
+ {variants_attributes: [:id, :name, :_destroy]},
197
+ {comments_attributes: [:id, :body, :_destroy]}
198
+ ]
199
+ end
200
+ ```
201
+
202
+ Plutonium extracts nested params via the form definition, not the policy. Hash entries here get iterated as field names by the form renderer and render as literal text inputs with names like `model[{:variants_attributes=>[...]}]`.
203
+
204
+ ```ruby
205
+ # ✅ Policy permits just the association name
206
+ def permitted_attributes_for_create
207
+ [:name, :variants]
208
+ end
209
+ ```
210
+
211
+ `nested_input :variants` in the definition handles the rest. See [Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs).
212
+
213
+ ### Auto-detection (dev only)
214
+
215
+ In development, undefined `permitted_attributes_for_*` methods auto-detect from the model. **Production raises** with a clear error:
216
+
217
+ ```
218
+ 🚨 Resource field auto-detection: PostPolicy#permitted_attributes_for_create
219
+ Auto-detected resource fields result in security holes and will fail outside of development.
220
+ ```
221
+
222
+ Always declare explicitly before deploying.
223
+
224
+ ## Association permissions
225
+
226
+ ```ruby
227
+ def permitted_associations
228
+ %i[comments tags author]
229
+ end
230
+ ```
231
+
232
+ Declares which associations get their own **tab on the show page**. When non-empty, the show page renders a tablist: a "Details" tab (the main field card + metadata aside) plus one tab per association — each lazy-loaded via a frame navigator panel pointing at the associated `has_many` collection, `has_one` record, or `belongs_to` target. When empty, the show page renders without tabs.
233
+
234
+ Each named association must:
235
+
236
+ - Exist on the model (raises `ArgumentError: unknown association ...` otherwise).
237
+ - Point to a class that's itself a registered Plutonium resource (raises `... is not a registered resource` otherwise).
238
+
239
+ This is **NOT** the same as:
240
+
241
+ - **Nested forms** — declared with `nested_input :variants` in the definition, requires `accepts_nested_attributes_for` on the model. See [Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs).
242
+ - **Association fields on tables / show details** — controlled by `permitted_attributes_for_index` / `_for_show` listing the association name.
243
+
244
+ ## Collection scoping (`relation_scope`)
245
+
246
+ Filter which records the user can see.
247
+
248
+ ### Always compose with `default_relation_scope`
249
+
250
+ 🚨 `relation_scope` MUST call `default_relation_scope(relation)` explicitly. Never `super` — the semantics depend on how ActionPolicy's DSL registered the scope. Plutonium enforces this at runtime via `verify_default_relation_scope_applied!`.
251
+
252
+ ```ruby
253
+ # ✅ Best — don't override at all. The inherited scope already calls default_relation_scope.
254
+
255
+ # ✅ Extra filters on top
256
+ relation_scope do |relation|
257
+ default_relation_scope(relation).where(archived: false)
258
+ end
259
+
260
+ # ✅ Role-based
261
+ relation_scope do |relation|
262
+ relation = default_relation_scope(relation)
263
+ user.admin? ? relation : relation.where(author: user)
264
+ end
265
+ ```
266
+
267
+ ### Wrong patterns
268
+
269
+ ```ruby
270
+ # ❌ Manually filtering by entity — bypasses default_relation_scope
271
+ relation_scope { |r| r.where(organization: current_scoped_entity) }
272
+
273
+ # ❌ Manual joins — same problem
274
+ relation_scope { |r| r.joins(:project).where(projects: {organization_id: current_scoped_entity.id}) }
275
+
276
+ # ❌ Missing default_relation_scope entirely — raises at runtime
277
+ relation_scope { |r| r.where(published: true) }
278
+ ```
279
+
280
+ ### What `default_relation_scope` does
281
+
282
+ 1. If a **parent** is present (nested resource), scopes via the parent association.
283
+ 2. Otherwise, applies `relation.associated_with(entity_scope)` for multi-tenancy.
284
+
285
+ Parent scoping takes precedence over entity scoping — the parent was already authorized and entity-scoped during its own authorization, so double-scoping isn't needed.
286
+
287
+ Full mechanics in [Tenancy › Entity scoping](/reference/tenancy/entity-scoping).
288
+
289
+ ### Intentionally skipping
290
+
291
+ Rare. Use `skip_default_relation_scope!` explicitly — never silently bypass:
292
+
293
+ ```ruby
294
+ relation_scope do |relation|
295
+ skip_default_relation_scope!
296
+ relation
297
+ end
298
+ ```
299
+
300
+ Before reaching for this, consider a separate, unscoped portal.
301
+
302
+ ## Portal-specific policies
303
+
304
+ ```ruby
305
+ class PostPolicy < ResourcePolicy
306
+ def create? = user.present?
307
+ end
308
+
309
+ # Admin — more permissive
310
+ class AdminPortal::PostPolicy < ::PostPolicy
311
+ include AdminPortal::ResourcePolicy
312
+
313
+ def destroy? = true
314
+ def permitted_attributes_for_create = %i[title content featured internal_notes]
315
+ end
316
+
317
+ # Public — read-only
318
+ class PublicPortal::PostPolicy < ::PostPolicy
319
+ include PublicPortal::ResourcePolicy
320
+ def create? = false
321
+ end
322
+ ```
323
+
324
+ ## Custom authorization context
325
+
326
+ ```ruby
327
+ # Policy
328
+ class PostPolicy < ResourcePolicy
329
+ authorize :department, allow_nil: true
330
+
331
+ def create? = department&.allows_posting?
332
+ end
333
+
334
+ # Controller
335
+ class PostsController < ResourceController
336
+ authorize :department, through: :current_department
337
+
338
+ private
339
+ def current_department = current_user.department
340
+ end
341
+ ```
342
+
343
+ ## Authorization errors
344
+
345
+ ```ruby
346
+ # Failed authorization raises ActionPolicy::Unauthorized
347
+
348
+ # Handle globally
349
+ rescue_from ActionPolicy::Unauthorized do
350
+ redirect_to root_path, alert: "Not authorized"
351
+ end
352
+ ```
353
+
354
+ ## Common patterns
355
+
356
+ ### Block archived records
357
+
358
+ ```ruby
359
+ def update? = !record.try(:archived?) && super
360
+ def destroy? = !record.try(:archived?) && super
361
+ ```
362
+
363
+ ### Owner-based
364
+
365
+ ```ruby
366
+ def update? = record.author == user || user.admin?
367
+ def destroy? = update?
368
+ ```
369
+
370
+ ### Role-based
371
+
372
+ ```ruby
373
+ def create? = user.admin? || user.editor?
374
+
375
+ def update?
376
+ return true if user.admin?
377
+ user.editor? && record.author == user
378
+ end
379
+ ```
380
+
381
+ ### Status-based
382
+
383
+ ```ruby
384
+ def update?
385
+ return false if record.archived?
386
+ owner? || admin?
387
+ end
388
+ ```
389
+
390
+ ### Time-based
391
+
392
+ ```ruby
393
+ def update?
394
+ return false if record.created_at < 24.hours.ago
395
+ owner?
396
+ end
397
+ ```
398
+
399
+ ## Debugging
400
+
401
+ ```ruby
402
+ # Console
403
+ user = User.find(1)
404
+ post = Post.find(1)
405
+
406
+ policy = PostPolicy.new(post, user: user)
407
+ policy.update?
408
+ policy.permitted_attributes_for_update
409
+ ```
410
+
411
+ ## Related
412
+
413
+ - [Controllers](./controllers) — call policies via `authorize_current!` and `authorized_resource_scope`
414
+ - [Interactions](./interactions) — custom actions whose policy methods you define
415
+ - [Resource › Actions](/reference/resource/actions) — registering actions that need policy methods
416
+ - [Tenancy › Entity scoping](/reference/tenancy/entity-scoping) — `default_relation_scope`, three model shapes, custom scopes
417
+ - [ActionPolicy docs](https://actionpolicy.evilmartians.io/) — the underlying library
@@ -1,49 +1,56 @@
1
- # Reference Documentation
2
-
3
- Complete API documentation for all Plutonium components.
4
-
5
- ## Core Components
6
-
7
- ### [Model](/reference/model/)
8
- Resource model configuration, features, and database integration.
9
-
10
- ### [Definition](/reference/definition/)
11
- How resources render - fields, columns, actions, search, and filters.
12
-
13
- ### [Policy](/reference/policy/)
14
- Authorization control - action permissions, attribute filtering, scoping.
15
-
16
- ### [Controller](/reference/controller/)
17
- HTTP handling, CRUD customization, and hooks.
18
-
19
- ### [Interaction](/reference/interaction/)
20
- Business logic encapsulation for custom actions.
21
-
22
- ## UI Components
23
-
24
- ### [Views](/reference/views/)
25
- Page classes, forms, tables, and display components.
26
-
27
- ### [Assets](/reference/assets/)
28
- TailwindCSS configuration, theming, and Stimulus controllers.
29
-
30
- ## Infrastructure
31
-
32
- ### [Generators](/reference/generators/)
33
- CLI generators for scaffolding resources, packages, and portals.
34
-
35
- ### [Portal](/reference/portal/)
36
- Portal configuration, authentication, and entity scoping.
37
-
38
- ## Quick Reference
39
-
40
- | I need to... | See |
41
- |--------------|-----|
42
- | Configure form fields | [Definition Fields](/reference/definition/fields) |
43
- | Add search/filters | [Definition Query](/reference/definition/query) |
44
- | Add custom buttons | [Definition Actions](/reference/definition/actions) |
45
- | Control access | [Policy](/reference/policy/) |
46
- | Create business logic | [Interaction](/reference/interaction/) |
47
- | Customize pages | [Views](/reference/views/) |
48
- | Style the UI | [Assets](/reference/assets/) |
49
- | Generate code | [Generators](/reference/generators/) |
1
+ # Reference
2
+
3
+ Concept-by-concept API documentation. For task-oriented walkthroughs, see [Guides](/guides/).
4
+
5
+ ## The seven areas
6
+
7
+ ### [App](/reference/app/)
8
+ Installation, packages (feature + portal), portal engines, mounting, route registration (including singular and custom routes), connecting resources via `pu:res:conn`, full generator catalog.
9
+
10
+ ### [Resource](/reference/resource/)
11
+ The four-layer resource — model, definition, query, actions. `pu:res:scaffold` field-type syntax, `has_cents`, SGID, URL routing, definition DSL (fields, inputs, displays, columns), page chrome, metadata panel, index views (table & grid), search, filters, scopes, sorting, custom + bulk actions.
12
+
13
+ ### [Behavior](/reference/behavior/)
14
+ Controllers, policies, interactions. Controller hooks (redirect, params, presentation), policy action methods and `permitted_attributes_for_*`, `permitted_associations`, `relation_scope`, interaction structure, outcomes, chaining, URL generation.
15
+
16
+ ### [UI](/reference/ui/)
17
+ Pages, forms, displays, tables, components, layouts, assets. Custom page classes, form field builders, association inputs (typeahead + inline `+`), built-in component kit, custom Phlex components, the shell, design tokens, `.pu-*` component classes, Phlexi themes.
18
+
19
+ ### [Auth](/reference/auth/)
20
+ Rodauth installation, account types (basic / admin / SaaS), profile resource with the SecuritySection component.
21
+
22
+ ### [Tenancy](/reference/tenancy/)
23
+ Multi-tenant entity scoping (`associated_with`, `default_relation_scope`, three model shapes), nested resources (parent/child routes, scoping), user invitations.
24
+
25
+ ### [Testing](/reference/testing/)
26
+ The `Plutonium::Testing::*` concerns — CRUD, policy matrix, definition smoke tests, model concerns, nested resources, portal access, interaction outcomes.
27
+
28
+ ## Quick reference
29
+
30
+ | I need to… | See |
31
+ |---|---|
32
+ | Install Plutonium | [App › Index](/reference/app/) |
33
+ | Run a generator | [App Generators](/reference/app/generators) |
34
+ | Create a portal | [App › Portals](/reference/app/portals) |
35
+ | Scaffold a resource | [App › Generators › `pu:res:scaffold`](/reference/app/generators#pu-res-scaffold) |
36
+ | Configure form fields | [Resource › Definition](/reference/resource/definition) |
37
+ | Add search / filters | [Resource › Query](/reference/resource/query) |
38
+ | Add custom buttons / bulk actions | [Resource › Actions](/reference/resource/actions) |
39
+ | Override CRUD redirects / params | [Behavior › Controllers](/reference/behavior/controllers) |
40
+ | Control who can see what | [Behavior › Policies](/reference/behavior/policies) |
41
+ | Write business logic | [Behavior › Interactions](/reference/behavior/interactions) |
42
+ | Customize a page | [UI › Pages](/reference/ui/pages) |
43
+ | Customize a form | [UI › Forms](/reference/ui/forms) |
44
+ | Style the UI | [UI › Assets](/reference/ui/assets) |
45
+ | Set up Rodauth | [Auth › Accounts](/reference/auth/accounts) |
46
+ | Add a profile page | [Auth › Profile](/reference/auth/profile) |
47
+ | Scope to a tenant | [Tenancy › Entity scoping](/reference/tenancy/entity-scoping) |
48
+ | Wire user invitations | [Tenancy › Invites](/reference/tenancy/invites) |
49
+ | Test a resource | [Testing](/reference/testing/) |
50
+
51
+ ## Reading this reference
52
+
53
+ - **🚨 Critical blocks** at the top of each page surface the "you'll regret this" rules. Skim them even if you're skimming the rest.
54
+ - **Option / DSL tables** are designed for scanning — find your option name without reading prose.
55
+ - **Cross-references** use VitePress relative paths. If a link points somewhere that doesn't exist yet, it's a known gap.
56
+ - **Concrete decision rules** ("use X when…, Y when…") sit alongside the option references. Reach for them when in doubt.