High-level guide for working with Plutonium applications - read this first
Entry point for all Plutonium work. Does three things:
pu:* generator. Hand-written files drift from conventions and break future generator runs.plutonium-tenancy. Don't reach for where(organization: ...) in a policy; fix the model instead.--dest=, --force (when re-running meta-generators), --auth=, --skip-bundle, --quiet so generators don't block on prompts. See Unattended execution.Plutonium applies Rails' bargain (follow the convention and the framework carries you; reach for an escape hatch when you need one) to the layer above CRUD: auth, authorization, multi-tenancy, admin UI, business operations. Four consequences change what you should actually type.
Not "defaults someone picked for you": computed from existing declarations:
| Derived | From |
|---|---|
| Field types, required markers, select choices | model columns, associations, attachments, enums, and validations (presence: true β required; inclusion: β select choices) |
| A collection's preloads (index, kanban, export) | the policy's permitted field set; there is no includes list to write or maintain |
| Tenant scope | your associations: direct belongs_to, then has_one/has_one :through, then reverse has_many |
| Action type (record / bulk / resource) | whether the interaction declares :resource, :resources, or neither |
| An association input's typeahead | the target resource's own search block |
| CRUD, nested and action routes | one register_resource line |
β Declare only what differs. A field :title matching the detected type is dead code, and one more line to fall out of step when the column changes. This is the single most common way generated-looking code goes wrong.
"Only admins see this field" is permitted_attributes_for_*. Never a definition declaration, and never a condition: (that only hides UI; the route stays live).
AdminPortal::PostDefinition < ::PostDefinition, and the same for policies and controllers. App-level default, portal-level subclass. No registry of overrides, no precedence DSL, no merge semantics, so "why does this field show here but not there" is always readable as a class hierarchy.
input :content, as: :markdowndisplay :priority do |f| β¦ end (a block, instance_execed in Phlex, emits markup directly and gives you f.object)as:; anything with its own constructor goes through a block (display :card do |field| β¦ end)create/update; page render_before_* / render_after_* instead of view_templateview_template on the nested class, or an ERB view at the controller path (ERB wins when both exist)Reaching for rung 5 on a rung-1 problem is how you end up owning breadcrumbs, the header and turbo frame wiring you never meant to touch.
Underneath all of it, it stays Rails. Models are plain ActiveRecord, controllers inherit from Rails controllers, views resolve through Rails view paths. A Plutonium resource and a hand-written controller coexist in one app.
A one-line request rarely says whether this is a new app, a half-built one, or a multi-tenant one, and those change which path you take. Spend 30 seconds reading the app before loading a bundle or running anything:
| Read | Tells you |
|---|---|
git log --oneline | head; is there a populated Gemfile + app/? |
Greenfield vs existing β install path (plutonium-app: base.rb, never plutonium.rb on an existing app) |
grep Gemfile for plutonium; ls config/packages.rb |
Already installed? β skip install |
ls packages/ |
What portals / feature packages already exist |
Does any model belongs_to an org/team/tenant? |
Multi-tenant β load plutonium-tenancy before declaring scoping |
This is the global "look before you leap"; each targeted skill carries its own ASK/CHECK gate for the specifics. Never run an installer or scaffold from a one-line request without first reading what's already there.
| Skill | Covers |
|---|---|
| [[plutonium-app]] | Installation, packages (feature + portal), portal engines, mounting, register_resource (including singular and custom routes), pu:res:conn |
| [[plutonium-resource]] | The resource itself: pu:res:scaffold, field types, model layer (Plutonium::Resource::Record, has_cents, SGID, routing), definition layer (fields/inputs/displays/columns, search/filters/scopes/sorting, custom actions, bulk actions, index views, page customization) |
| [[plutonium-behavior]] | Controllers (hooks, key methods, presentation), policies (action methods, permitted_attributes_for_*, permitted_associations), interactions (structure, outcomes, chaining, URL generation) |
| [[plutonium-async-interactions]] | Async interactions: async, the Run STI model, failure policies (halt/continue/transactional), authorization re-derivation at perform time, registering AsyncRun as a resource (progress page + running banner), scheduling ReapJob |
| [[plutonium-ui]] | Page classes, forms, displays, tables, custom Phlex components, layouts, modals & tabs, Tailwind config, Stimulus, design tokens, .pu-* classes, Phlexi themes |
| [[plutonium-kanban]] | kanban doβ¦end DSL in a Definition, columns, card_fields, position_on, realtime, column actions, kanban_move? policy, quick-add, static vs dynamic boards |
| [[plutonium-dashboard]] | Dashboards, Plutonium::Dashboard::Base, metric / chart / card, register_dashboard, lazy turbo-frame cards, refresh, condition:, authorize?, pu:dashboard |
| [[plutonium-auth]] | Rodauth install, account types (basic / admin / SaaS), profile resource, security section |
| [[plutonium-tenancy]] | Entity scoping (associated_with, default_relation_scope, three model shapes), nested resources, invites |
| [[plutonium-testing]] | pu:test:install, pu:test:scaffold, ResourceCrud/ResourcePolicy/ResourceDefinition/ResourceModel/NestedResource/PortalAccess/ResourceInteraction, AuthHelpers |
| [[plutonium-wizard]] | Multi-step flows: the wizard DSL (step/review/using:/condition:, per-step on_submit/persist/on_rollback, execute), anchoring & resume, one-time wizards + gate, registration (wizard macro + register_wizard), storage/config + SweepJob |
Triggers: installing Plutonium, building a new app, adding the first resource in a new domain, setting up a new portal or package, "build me a Y app", "set up X from scratch".
Load these before writing code:
plutonium-app: install, portals, packages, routes.plutonium-resource: scaffold, model, definition (the bulk of the work).plutonium-behavior: controllers, policies, interactions.plutonium-tenancy: only if multi-tenant; load before declaring entity scoping.Add when relevant:
plutonium-auth for login / accounts / profile.plutonium-ui for custom pages, forms, components, or theming.plutonium-testing when scaffolding tests.| About to⦠| Load |
|---|---|
| Install Plutonium, create a portal or package, mount engines, register routes (incl. singular / custom routes) | [[plutonium-app]] |
Run pu:res:scaffold, pick field types, set scaffold options |
[[plutonium-resource]] |
Edit a model, add associations, use has_cents, override to_param / to_label |
[[plutonium-resource]] |
| Edit a definition, fields, inputs, displays, columns, search, filters, scopes, custom actions, bulk actions, index views, modal/slideover, page titles | [[plutonium-resource]] |
Override a controller action, hook, redirect, or resource_params |
[[plutonium-behavior]] |
Write relation_scope, permitted_attributes_for_*, permitted_associations, action methods, or any policy override |
[[plutonium-behavior]] (+ [[plutonium-tenancy]] if scoping) |
| Write an interaction class for business logic | [[plutonium-behavior]] |
Make a bulk/long-running interaction async (async), or schedule the stalled-run reaper |
[[plutonium-async-interactions]] |
Scope a model to a tenant, write associated_with, set portal entity strategy |
[[plutonium-tenancy]] |
| Configure parent/child nested routes, custom parent resolution | [[plutonium-tenancy]] |
| Set up user invitations or entity membership | [[plutonium-tenancy]] |
Build or customize a kanban board view, kanban doβ¦end, columns, card_fields, position_on, realtime, column actions, kanban_move? policy |
[[plutonium-kanban]] |
Build a dashboard, KPI overview or chart page, pu:dashboard, metric / chart / card, register_dashboard, refresh, per-card conditions |
[[plutonium-dashboard]] |
Build a custom page (override ShowPage/IndexPage/NewPage/EditPage), custom form, custom display, custom table, custom Phlex component |
[[plutonium-ui]] |
| Configure Tailwind, register Stimulus controllers, edit design tokens, theme forms/displays/tables, write a custom layout | [[plutonium-ui]] |
| Install Rodauth, set up accounts, configure login flow, add the profile resource | [[plutonium-auth]] |
Write tests for a resource, run pu:test:scaffold, include Plutonium::Testing::* concerns |
[[plutonium-testing]] |
Build a multi-step flow, onboarding, checkout, branching create, register a wizard / register_wizard, gate a one-time wizard |
[[plutonium-wizard]] |
A resource is four cooperating layers; Plutonium auto-fills defaults from the model, so you only declare overrides:
| Layer | File | Purpose |
|---|---|---|
| Model | app/models/post.rb |
Data, validations, associations |
| Definition | app/definitions/post_definition.rb |
UI, fields, filters, actions |
| Policy | app/policies/post_policy.rb |
Authorization: who, what |
| Controller | app/controllers/posts_controller.rb |
Request handling (rarely edited; use hooks) |
Plus one optional fifth layer:
| Layer | File | Purpose |
|---|---|---|
| Interaction | app/interactions/publish_post_interaction.rb |
Business logic for custom actions |
Every Plutonium generator is discoverable via rails g pu:<tab>. Always pass --dest= to skip prompts.
| Generator | Purpose | Skill |
|---|---|---|
pu:core:install |
Initial Plutonium setup | plutonium-app |
pu:core:assets |
Custom Tailwind + Stimulus toolchain | plutonium-ui |
pu:res:scaffold NAME field:type ... |
New resource (model, migration, controller, policy, definition) | plutonium-resource |
pu:res:conn RESOURCE --dest=PORTAL |
Connect resource to a portal | plutonium-app |
pu:pkg:package NAME |
Feature package | plutonium-app |
pu:pkg:portal NAME --auth=... --scope=... |
Portal package | plutonium-app |
pu:rodauth:install |
Install Rodauth base | plutonium-auth |
pu:rodauth:account NAME |
Basic Rodauth account | plutonium-auth |
pu:rodauth:admin NAME |
Hardened admin account (2FA, lockout, audit) | plutonium-auth |
pu:saas:setup --user ... --entity ... |
Meta: user + entity + membership + portal + profile + welcome + invites | plutonium-auth + plutonium-tenancy |
pu:saas:user / :entity / :membership / :portal / :welcome |
Individual SaaS pieces | plutonium-auth + plutonium-app |
pu:profile:install / :setup / :conn |
Profile resource + security section | plutonium-auth |
pu:invites:install |
User invitations package | plutonium-tenancy |
pu:invites:invitable NAME |
Mark a model as invitable | plutonium-tenancy |
pu:eject:layout |
Eject base layout for customization | plutonium-ui |
pu:eject:shell |
Eject topbar/sidebar partials | plutonium-ui |
pu:test:install |
Install Plutonium::Testing scaffolding |
plutonium-testing |
pu:test:scaffold NAME --portals=... |
Scaffold integration tests | plutonium-testing |
pu:dashboard NAME --dest=PORTAL [--at=/] |
Dashboard class + register_dashboard route |
plutonium-dashboard |
pu:skills:sync |
Sync Plutonium Claude skills into the project | (this skill) |
Plutonium generators are interactive by default. For scripts, agents, or CI:
| Flag | Generators | Purpose |
|---|---|---|
--dest=main_app / --dest=<package> |
pu:res:scaffold, pu:res:conn, package-targeted generators |
Skip "select destination" prompt |
--force |
any | Overwrite conflicting files (required when re-running pu:saas:setup or meta-generators) |
--auth=<account> / --public / --byo |
pu:pkg:portal |
Skip auth-type prompt |
--skip-bundle |
gem-installing generators | Avoid mid-run bundle install |
--quiet |
most | Reduce output noise |
Meta-generators (pu:saas:setup) propagate flags to the generators they chain. Always pass --force when re-running a meta-generator on an app that already has some of its outputs.
rails g pu:res:scaffold Model field:type ... --dest=main_app.rails db:prepare.rails g pu:res:conn Model --dest=portal_name.