---
url: https://radioactive-labs.github.io/plutonium-core/guides/creating-packages.md
---
# 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

```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](/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 — how fields render per portal
class AdminPortal::PostDefinition < ::PostDefinition
  scope :pending_review
  input :internal_notes, hint: "Not shown to the author"
end

# Policy — which fields exist, and who may act
#          `internal_notes` appears for admins because THIS permits it,
#          not because the definition above mentions it.
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`.

## Related

* [Reference › App › Packages](/reference/app/packages) — full package surface
* [Reference › App › Portals](/reference/app/portals) — portal-specific configuration
* [Adding resources](./adding-resources) — `pu:res:scaffold` and `pu:res:conn`
* [Authentication](./authentication) — portal auth setup
