plutonium-portal
Plutonium portals - web interfaces with authentication, entity scoping, and routes
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Plutonium Portals
Portals are Rails engines that provide web interfaces for specific user types.
Creating a Portal
rails g pu:pkg:portal dashboard
Generator Options
| Option | Description |
|---|---|
--auth=NAME |
Rodauth account to authenticate with (e.g., --auth=user) |
--public |
Grant public access (no authentication) |
--byo |
Bring your own authentication |
# Non-interactive examples
rails g pu:pkg:portal admin --auth=admin
rails g pu:pkg:portal api --public
rails g pu:pkg:portal custom --byo
Without flags, the generator prompts interactively:
- Rodauth account - Use existing Rodauth authentication
- Public access - No authentication required
-
Bring your own - Implement custom
current_user
Portal Engine
# packages/dashboard_portal/lib/engine.rb
module DashboardPortal
class Engine < Rails::Engine
include Plutonium::Portal::Engine
config.after_initialize do
# Optional: multi-tenancy
scope_to_entity Organization, strategy: :path
end
end
end
Authentication
Rodauth Integration
# packages/dashboard_portal/app/controllers/dashboard_portal/concerns/controller.rb
module DashboardPortal
module Concerns
module Controller
extend ActiveSupport::Concern
include Plutonium::Portal::Controller
include Plutonium::Auth::Rodauth(:user) # Use :user account
end
end
end
Public Access
module DashboardPortal
module Concerns
module Controller
extend ActiveSupport::Concern
include Plutonium::Portal::Controller
include Plutonium::Auth::Public
end
end
end
Custom Authentication
module DashboardPortal
module Concerns
module Controller
extend ActiveSupport::Concern
include Plutonium::Portal::Controller
include Plutonium::Auth::Public
def current_user
@current_user ||= User.find_by(api_key: request.headers["X-API-Key"])
end
end
end
end
Entity Scoping (Multi-tenancy)
Automatically scope all data to a parent entity.
Path Strategy
Entity ID in URL path:
module AdminPortal
class Engine < Rails::Engine
include Plutonium::Portal::Engine
config.after_initialize do
scope_to_entity Organization, strategy: :path
end
end
end
Routes become: /organizations/:organization_id/posts
Custom Strategy
Implement your own lookup method:
module AdminPortal
class Engine < Rails::Engine
include Plutonium::Portal::Engine
config.after_initialize do
scope_to_entity Organization, strategy: :current_organization
end
end
end
# In controller concern
module AdminPortal
module Concerns
module Controller
extend ActiveSupport::Concern
include Plutonium::Portal::Controller
private
# Method name must match strategy
def current_organization
@current_organization ||= Organization.find_by!(subdomain: request.subdomain)
end
end
end
end
Accessing the Scoped Entity
current_scoped_entity # The current Organization/Account/etc.
scoped_to_entity? # true if scoping is active
Model Requirements
Models must have an association path to the scoped entity:
# Direct association (preferred)
class Post < ResourceRecord
belongs_to :organization
end
# Through association
class Comment < ResourceRecord
belongs_to :post
has_one :organization, through: :post
end
# Complex (define custom scope)
class AuditLog < ResourceRecord
scope :associated_with_organization, ->(org) {
joins(:user).where(users: { organization_id: org.id })
}
end
Routes
Portal Routes
# packages/dashboard_portal/config/routes.rb
DashboardPortal::Engine.routes.draw do
root to: "dashboard#index"
# Register resources
register_resource ::Post
register_resource Blogging::Comment
# Custom routes
get "settings", to: "settings#index"
end
Custom Routes on Resources
Add member or collection routes with a block:
register_resource ::Post do
member do
get :preview
get :analytics
post :publish
end
collection do
get :archived
post :bulk_publish
end
end
This generates:
-
GET /posts/:id/preview -
GET /posts/:id/analytics -
POST /posts/:id/publish -
GET /posts/archived -
POST /posts/bulk_publish
Mounting in Main App
# config/routes.rb
Rails.application.routes.draw do
# With authentication constraint
constraints Rodauth::Rails.authenticate(:user) do
mount DashboardPortal::Engine, at: "/dashboard"
end
# Or without
mount PublicPortal::Engine, at: "/public"
end
Controller Hierarchy
Resource Controllers
Portal resource controllers inherit from the feature package's controller:
::PostsController (feature package controller)
↓
DashboardPortal::PostsController (portal-specific)
Controllers are auto-created if not defined. When accessing DashboardPortal::PostsController:
- If file exists, use it
- Otherwise, dynamically create inheriting from
::PostsController - Include
DashboardPortal::Concerns::Controller
Non-Resource Controllers
For portal pages not tied to a resource (dashboard, settings, etc.), inherit from PlutoniumController:
# packages/dashboard_portal/app/controllers/dashboard_portal/dashboard_controller.rb
module DashboardPortal
class DashboardController < PlutoniumController
def index
# Dashboard home page
end
end
end
Portal-Specific Overrides
Override Definition
# packages/dashboard_portal/app/definitions/dashboard_portal/post_definition.rb
class DashboardPortal::PostDefinition < ::PostDefinition
# Hide certain actions from this portal
# Add portal-specific scopes
scope :my_posts, -> { where(user: current_user) }
end
Override Policy
# packages/dashboard_portal/app/policies/dashboard_portal/post_policy.rb
class DashboardPortal::PostPolicy < ::PostPolicy
include DashboardPortal::ResourcePolicy
def destroy?
false # No deletion in user portal
end
def permitted_attributes_for_create
%i[title content] # Fewer fields than admin
end
end
Override Controller
# packages/dashboard_portal/app/controllers/dashboard_portal/posts_controller.rb
class DashboardPortal::PostsController < ::PostsController
include DashboardPortal::Concerns::Controller
private
def preferred_action_after_submit
"index"
end
end
Layout and Views
Portal Layout
<!-- packages/dashboard_portal/app/views/layouts/dashboard_portal.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag "application" %>
</head>
<body>
<nav><!-- Portal navigation --></nav>
<main><%= yield %></main>
</body>
</html>
Dashboard Controller
# packages/dashboard_portal/app/controllers/dashboard_portal/dashboard_controller.rb
module DashboardPortal
class DashboardController < PlutoniumController
def index
# Dashboard home page
end
end
end
Multiple Portals Example
# Admin portal - full access
module AdminPortal
class Engine < Rails::Engine
include Plutonium::Portal::Engine
config.after_initialize do
scope_to_entity Organization, strategy: :path
end
end
end
# User dashboard - limited access
module DashboardPortal
class Engine < Rails::Engine
include Plutonium::Portal::Engine
config.after_initialize do
scope_to_entity Organization, strategy: :path
end
end
end
# Public portal - read-only, no auth
module PublicPortal
class Engine < Rails::Engine
include Plutonium::Portal::Engine
end
end
Each portal can:
- Have different authentication
- Show different fields
- Allow different actions
- Use different layouts
Related Skills
-
plutonium-package- Package overview (features vs portals) -
plutonium-rodauth- Authentication setup and configuration -
plutonium-connect-resource- Connecting resources to portals -
plutonium-policy- Portal-specific policies -
plutonium-definition- Portal-specific definitions -
plutonium-controller- Portal-specific controllers
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!