User Profile โ
Add a profile / account-settings page where users edit personal fields and access Rodauth security features (change password, 2FA, etc.) in one place.
Goal โ
A /profile URL that shows the user's personal fields plus a "Security" section linking to Rodauth-managed features.
๐จ Critical โ
- Profile association is always
:profileregardless of the model class:current_user.profile,build_profile,params.require(:profile)always work. - Profile needs
pu:profile:connto be visible. Without it there is no/profileroute, andprofile_urlstaysnilso the user menu has no Profile link. - A user without a profile row is fine.
profile_urlsends them to the new-profile form. Creating the row up front is optional (see step 4).
The show page renders the user's fields, then the Security Settings block with Rodauth-backed actions:

Editing produces a regular Plutonium form, same form generator the rest of your resources use:

Quick path โ
rails g pu:profile:setup date_of_birth:date bio:text \
--dest=competition \
--portal=competition_portalpu:profile:setup is a meta-generator: runs pu:profile:install + pu:profile:conn in one shot.
Step-by-step โ
1. Install โ
rails generate pu:profile:install bio:text avatar:attachment 'timezone:string?' \
--dest=customer| Option | Default | Description |
|---|---|---|
--dest=DEST | (prompts) | Target package or main_app |
--user-model=NAME | User | Rodauth user model |
Custom resource name (first positional argument):
rails g pu:profile:install AccountSettings bio:text --dest=main_appBy default the model is {UserModel}Profile (UserProfile, StaffUserProfile, etc.).
2. Migrate โ
rails db:prepare3. Connect to a portal โ
rails g pu:profile:conn --dest=customer_portalThis registers the profile as a singular resource: exposes /profile (no :id) and the profile_url helper.
It also generates a portal policy scoped to the current user: create? is user.profile.nil? (one profile per user), update? is true, and destroy? is false. The controller sets user from current_user, so :user is not a permitted attribute.
Profile can't be edited?
Policies generated by older versions of pu:profile:conn lack update?, so it falls back to create? and the profile becomes read-only once it exists. Add def update? = true to the portal's profile policy.
4. (Optional) Create the row up front โ
Without a profile row, the first visit to the profile is the "create profile" form, and the Security section appears once it is saved. To land users on the show page from the first click, create the row when the user is created:
# app/models/user.rb (has_one added by pu:profile:install)
class User < ApplicationRecord
has_one :profile, class_name: "UserProfile", dependent: :destroy
after_create :create_profile
endand backfill existing users:
rails runner "User.find_each { |u| u.profile || u.create_profile }"Skip this when the profile has required fields the user must fill in, or when other code already creates profiles.
What you get โ
The generated definition injects a custom ShowPage that renders SecuritySection: dynamically lists Rodauth security links based on which features are enabled:
| Feature enabled | Link rendered |
|---|---|
change_password | Change Password |
change_login | Change Email |
otp | Two-Factor Authentication |
recovery_codes | Recovery Codes |
webauthn | Security Keys |
active_sessions | Active Sessions |
close_account | Close Account |
If a feature isn't enabled, its link doesn't render; no configuration needed.
Linking to the profile โ
The topbar avatar menu already shows a "Profile" entry once pu:profile:conn has run. Plutonium::Auth::Rodauth defines profile_url as nil, and the generator overrides it in the portal's controller concern. To link from your own views:
link_to("Profile", profile_url) if profile_urlAccount features on the profile page โ
Per-user settings that aren't profile fields (API tokens, connected accounts, notification preferences) belong on the profile page. Link them from the same ShowPage hook, next to SecuritySection:
class ShowPage < ShowPage
private
def render_after_content
render Plutonium::Profile::SecuritySection.new
div(class: "mt-8") do
a(href: resource_url_for(ApiToken), class: "font-medium text-[var(--pu-text)] hover:underline") { "API tokens" }
end
end
endSee Reference โบ Auth โบ Profile for scoping such a resource to the user.
Customizing the definition โ
The generated profile is a normal Plutonium definition. Customize like any other:
class UserProfileDefinition < Plutonium::Resource::Definition
field :bio, as: :markdown
input :avatar, as: :uppy
field :timezone, as: :select, choices: ActiveSupport::TimeZone.all.map(&:name)
metadata :created_at, :updated_at
class ShowPage < ShowPage
private
def render_after_content
render Plutonium::Profile::SecuritySection.new
end
end
endSee Reference โบ Resource โบ Definition for the full definition surface.
Multiple account types โ
If your app has both User and StaffUser accounts, run pu:profile:install once per:
rails g pu:profile:install --user-model=User --dest=main_app
rails g pu:profile:install --user-model=StaffUser --dest=main_appEach gets its own *Profile model with :profile association on the respective user. Connect each to the appropriate portal:
rails g pu:profile:conn UserProfile --dest=customer_portal
rails g pu:profile:conn StaffUserProfile --dest=admin_portalCommon issues โ
- First visit shows a "create profile" form. The user has no profile row yet. That's expected; see step 4 if they should land on the show page.
- Profile is read-only after creation. The policy predates the generated
update?. Adddef update? = true. - No Profile link in the user menu. The profile isn't connected to this portal, so
profile_urlreturnsnil. Runpu:profile:conn --dest=<portal>. SecuritySectionshows nothing: none of the relevant Rodauth features are enabled. Enablechange_password,otp, etc. on the Rodauth plugin.
Related โ
- Reference โบ Auth โบ Profile: full surface
- Reference โบ Auth โบ Accounts: Rodauth feature flags that gate SecuritySection
- Authentication: the underlying auth setup
