Skip to content

Nested Resources

Set up parent/child relationships so /companies/:id/nested_properties works automatically.

Goal

Company has_many :properties, and you want:

  • A "Properties" tab on the Company show page.
  • A nested URL /companies/123/nested_properties for the company's properties.
  • Forms that auto-fill the parent (no manual hidden field).
  • Queries scoped to the parent (sibling companies' properties invisible).

All of this happens with no manual route wiring — Plutonium generates it from the association.

Steps

1. Scaffold parent and child

bash
rails g pu:res:scaffold Company name:string --dest=main_app
rails g pu:res:scaffold Property company:belongs_to name:string --dest=main_app
rails db:prepare

2. Connect both to the portal

bash
rails g pu:res:conn Company Property --dest=admin_portal

Plutonium reads the has_many :properties association on Company and registers nested routes for Property automatically.

3. (Optional) Expose the relationship on the Company show page

ruby
class CompanyPolicy < ResourcePolicy
  def permitted_associations
    %i[properties]
  end
end

This adds a "Properties" tab on the Company show page that loads the nested collection. See Reference › Behavior › Policies › Association permissions.

Parent show page with nested-association tab

4. Visit the URL

/admin/companies/1/nested_properties

Properties index, scoped to Company #1. Forms hide the company field (already determined by URL).

Generated routes

Plutonium prefixes nested routes with nested_ so they don't conflict with top-level:

RoutePurpose
/companies/:company_id/nested_propertieshas_many index
/companies/:company_id/nested_properties/newnew
/companies/:company_id/nested_properties/:idshow
/companies/:company_id/nested_company_profilehas_one show (no :id)
/companies/:company_id/nested_company_profile/newhas_one new

has_one associations get singular routes — index redirects to show (or new if no record exists).

What Plutonium does automatically

  1. Resolves the parent via current_parent, authorized for :read?.
  2. Scopes queries via the parent association (company.properties for has_many; where(company_id: ...) for has_one).
  3. Assigns the parent on create (injected into resource_params).
  4. Hides the parent field in forms and displays.

No hidden fields. No manual scoping.

URL generation

Use resource_url_for with the parent: option:

ruby
resource_url_for(Property, parent: company)
# => /admin/companies/123/nested_properties

resource_url_for(property, parent: company)
# => /admin/companies/123/nested_properties/456

resource_url_for(Property, action: :new, parent: company)
resource_url_for(property, action: :edit, parent: company)

# Interactions compose with parent
resource_url_for(property, parent: company, interaction: :archive)
resource_url_for(Property, parent: company, interaction: :bulk_delete, ids: [1, 2])

Common patterns

Show parent on standalone listings

By default, the parent field is hidden in forms/displays (it's in the URL). To show it on the standalone (non-nested) listing:

ruby
class PropertiesController < ::ResourceController
  private
  def present_parent? = current_parent.nil?
end

Custom parent resolution (e.g. by slug)

ruby
def current_parent
  @current_parent ||= Company.friendly.find(params[:company_id])
end

Compound uniqueness within parent

ruby
class Property < ResourceRecord
  belongs_to :company
  validates :code, uniqueness: {scope: :company_id}
end

Without the scope, the same code in different companies would collide.

Custom routes on nested resources

ruby
register_resource ::Property do
  member do
    get  :analytics, as: :analytics    # `as:` is REQUIRED
    post :archive,   as: :archive
  end
end

Always pass as:

Without as:, resource_url_for(property, parent: company, action: :analytics) fails — no named route to look up.

Policy authorization context

The child policy automatically receives the parent:

ruby
class PropertyPolicy < ResourcePolicy
  # parent              => the Company instance
  # parent_association  => :properties

  def create?
    parent.present? && user.member_of?(parent)
  end
end

The parent is authorized for :read? before current_parent returns — children inherit the parent's access requirements.

Parent scoping vs entity scoping

When a parent is present, parent scoping wins: default_relation_scope scopes via the parent association, NOT entity_scope. The parent was already entity-scoped during its own authorization — double-scoping isn't needed.

In the child policy, just call default_relation_scope — it handles both cases:

ruby
relation_scope do |relation|
  default_relation_scope(relation)    # parent when present, entity_scope otherwise
end

See Reference › Tenancy › Nested resources › Parent vs entity scoping.

Nesting limitations

Plutonium supports one level of nesting only:

  • /companies/:company_id/nested_properties (parent → child)
  • /companies/:company_id/nested_properties/:property_id/nested_units (grandparent → parent → child)

For deeper hierarchies, use top-level routes plus association tabs on the show page (permitted_associations).

Nested inputs (sub-records inside a parent form)

A different feature with a confusingly similar name. Nested resources (above) give you separate URLs for the child collection. Nested inputs let you edit child records inline inside the parent's form — a single submit creates/updates/deletes them in one go, backed by Rails' accepts_nested_attributes_for.

Use nested inputs when the children are conceptually part of the parent (line items on an order, variants on a product, contact methods on a person) and don't deserve their own page.

Setup

ruby
# Model
class Post < ResourceRecord
  has_many :comments
  accepts_nested_attributes_for :comments, allow_destroy: true, reject_if: :all_blank
end

# Definition
class PostDefinition < ResourceDefinition
  nested_input :comments do |definition|
    definition.input :body, as: :text
  end
end

# Policy — list the association name (NOT `comments_attributes`)
class PostPolicy < ResourcePolicy
  def permitted_attributes_for_create
    [:title, :body, :comments]
  end
end

Permit the association, not the strong-params shape

List :comments in permitted_attributes_for_* — Plutonium translates it to comments_attributes: [...] for you. If you write the raw hash, the form renders the field name as a literal label instead of the nested editor.

Result

Inline nested-input editor with Add Comment button

Each existing child renders as its own sub-form. A Delete checkbox appears when allow_destroy: true; the + Add button appends a blank row.

Sourcing fields from an existing definition

If the child already has its own Definition and you want to reuse its inputs:

ruby
nested_input :comments, using: CommentDefinition, fields: %i[author body]

Limits

ruby
nested_input :variants, limit: 5      # cap the number of children
nested_input :profile,  macro: :has_one   # singular sub-form, no Add button

Nested inputs vs nested resources

Nested inputs (nested_input :comments)Nested resources (this guide's main topic)
URLNone — inline in parent form/posts/:id/nested_comments
SubmitOne — saves parent + children togetherIndependent CRUD per child
DiscoverabilityAlways visible in parent formTab on parent show page (with permitted_associations)
Best forTightly-owned children (line items, variants)Children users browse on their own (orders, posts)
Backingaccepts_nested_attributes_forPlutonium's nested controller routing

You can use both on the same association — they're not mutually exclusive.

Inline + add on the parent form

When a form has an association select (e.g. picking the company on a Property form), the inline + button next to the select opens the parent's :new action. If the parent form is already in a modal, the + opens a stacked secondary modal so the in-progress form isn't lost. See Reference › UI › Forms › Association inputs.

Common issues

  • Nested route doesn't exist — both parent AND child must be registered in the same portal (pu:res:conn).
  • Parent shows up in the form anyway — check present_parent? / submit_parent? on the controller. Default is to hide on nested routes.
  • Multiple belongs_to to the same parent class (e.g. Match belongs_to :home_team, :away_team) — Plutonium raises. Override scoped_entity_association to specify. See Reference › Tenancy › Entity scoping.
  • resource_url_for returns wrong URL for a nested resource — check that custom routes use as:.

Released under the MIT License.