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
| Type | Purpose | Generator | Examples |
|---|---|---|---|
| Feature | Business logic | pu:pkg:package NAME | blogging, billing, inventory |
| Portal | Web interface | pu:pkg:portal NAME | admin_portal, customer_portal, public_portal |
🚨 Don't mix the two. Feature packages have NO routes, views, or controllers. Portal packages have NO models or interactions.
Feature package
1. Generate
rails g pu:pkg:package blogging2. Structure
packages/blogging/
├── app/
│ ├── models/blogging/ # Blogging::Post
│ ├── definitions/blogging/ # Blogging::PostDefinition
│ ├── policies/blogging/ # Blogging::PostPolicy
│ └── interactions/blogging/ # Blogging::PublishPostInteraction
├── db/migrate/
└── lib/engine.rb3. Create resources inside it
rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
rails db:migrate4. Expose it via a portal
rails g pu:res:conn Blogging::Post --dest=admin_portalPortal package
1. Generate
rails g pu:pkg:portal admin --auth=userOptions:
--auth=NAME— Rodauth account to authenticate with.--public— public access, no auth.--byo— bring your own auth.--scope=CLASS— entity class for multi-tenancy.
2. Mount it
# config/routes.rb
Rails.application.routes.draw do
constraints Rodauth::Rails.authenticate(:user) do
mount AdminPortal::Engine, at: "/admin"
end
end3. Connect resources
rails g pu:res:conn Post Blogging::Post --dest=admin_portalSee 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::Postapp/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
rails g pu:res:scaffold Comment user:belongs_to blogging/post:belongs_to body:text --dest=commentsThe 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 (
billingshouldn't depend onblogging).
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 dashboardThe 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:
# config/packages.rb
Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
load package
endLoaded from config/application.rb. Migrations from all packages are picked up by rails db:migrate automatically.
Per-portal overrides
# 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]
endCommon issues
- Class not loading — namespace must match the directory:
app/models/blogging/post.rbMUST beBlogging::Post. - Migration not running — package migrations are auto-included. If they aren't running, check
config/packages.rbis loaded fromapplication.rb. - Cross-package association fails — use
blogging/post:belongs_toinpu:res:scaffold, OR manually setclass_name: "Blogging::Post"on thebelongs_to.
Related
- Reference › App › Packages — full package surface
- Reference › App › Portals — portal-specific configuration
- Adding resources —
pu:res:scaffoldandpu:res:conn - Authentication — portal auth setup
