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 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 โ
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:prepare4. 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.
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 โ
rails g pu:res:conn Blogging::Post --dest=admin_portalYou can connect multiple resources in one command:
rails g pu:res:conn Blogging::Post Blogging::Comment --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
