Skip to content

Creating Packages โ€‹

Organize your app into feature and portal packages.

Goal โ€‹

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.

Two types โ€‹

TypePurposeGeneratorExamples
FeatureBusiness logicpu:pkg:package NAMEblogging, billing, inventory
PortalWeb interfacepu:pkg:portal NAMEadmin_portal, customer_portal, public_portal

๐Ÿšจ 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.

Feature package โ€‹

1. Generate โ€‹

bash
rails g pu:pkg:package blogging

2. Structure โ€‹

packages/blogging/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ models/blogging/             # Blogging::Post
โ”‚   โ”œโ”€โ”€ definitions/blogging/        # Blogging::PostDefinition
โ”‚   โ”œโ”€โ”€ policies/blogging/           # Blogging::PostPolicy
โ”‚   โ””โ”€โ”€ interactions/blogging/       # Blogging::PublishPostInteraction
โ”œโ”€โ”€ db/migrate/
โ””โ”€โ”€ lib/engine.rb

3. Create resources inside it โ€‹

bash
rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
rails db:prepare

4. Expose it via a portal โ€‹

bash
rails g pu:res:conn Blogging::Post --dest=admin_portal

Portal package โ€‹

1. Generate โ€‹

bash
rails g pu:pkg:portal admin --auth=user

Options:

  • --auth=NAME โ€” Rodauth account to authenticate with.
  • --public โ€” public access, no auth.
  • --byo โ€” bring your own auth.
  • --scope=CLASS โ€” entity class for multi-tenancy.

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.

2. Connect resources โ€‹

bash
rails g pu:res:conn Blogging::Post --dest=admin_portal

You can connect multiple resources in one command:

bash
rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal

See Reference โ€บ App โ€บ Portals for the full portal surface.

Auto-namespacing โ€‹

Every file under app/<kind>/blogging/ resolves to Blogging::*:

  • app/models/blogging/post.rb โ†’ Blogging::Post
  • app/policies/blogging/post_policy.rb โ†’ Blogging::PostPolicy

Each feature package gets base classes โ€” Blogging::ApplicationRecord, Blogging::ResourceRecord, Blogging::ResourcePolicy, Blogging::ResourceDefinition, Blogging::ResourceInteraction โ€” that inherit from the main app's.

Cross-package references โ€‹

bash
rails g pu:res:scaffold Comment user:belongs_to blogging/post:belongs_to body:text --dest=comments

The blogging/post syntax expands to Blogging::Post.

When to use which โ€‹

Feature package โ€‹

When the code:

  • Could be reused across multiple portals (admin and customer both edit Blogging::Post).
  • Has no inherent UI / auth.
  • You want isolated from other domains (billing shouldn't depend on blogging).

Portal package โ€‹

When the code:

  • Has a specific auth flow (admin vs customer vs public).
  • Renders different views of the same underlying resources.
  • Needs different policies / definitions per audience.

When NOT to make a package โ€‹

For an app that doesn't need cross-portal sharing, just put resources in --dest=main_app. Packages add organization, not power.

Typical architecture โ€‹

packages/
โ”œโ”€โ”€ blogging/                # Feature: blog functionality
โ”œโ”€โ”€ billing/                 # Feature: payments/invoicing
โ”œโ”€โ”€ admin_portal/            # Portal: admin interface
โ””โ”€โ”€ customer_portal/         # Portal: customer dashboard

The portals expose the features. A single feature can be exposed by multiple portals โ€” usually with different policies and definitions per portal.

Package loading โ€‹

Generated by pu:core:install:

ruby
# config/packages.rb
Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
  load package
end

Loaded from config/application.rb. Migrations from all packages are picked up by rails db:migrate automatically.

Per-portal overrides โ€‹

ruby
# Definition โ€” different fields per portal
class AdminPortal::PostDefinition < ::PostDefinition
  input :internal_notes, as: :text     # admins see this; customers don't
  scope :pending_review
end

# Policy โ€” different rules per portal
class AdminPortal::PostPolicy < ::PostPolicy
  include AdminPortal::ResourcePolicy

  def destroy? = true
  def permitted_attributes_for_create = %i[title content featured internal_notes]
end

Common issues โ€‹

  • Class not loading โ€” namespace must match the directory: app/models/blogging/post.rb MUST be Blogging::Post.
  • Migration not running โ€” package migrations are auto-included. If they aren't running, check config/packages.rb is loaded from application.rb.
  • Cross-package association fails โ€” use blogging/post:belongs_to in pu:res:scaffold, OR manually set class_name: "Blogging::Post" on the belongs_to.

Released under the MIT License.