Organizations & Teams
RailsFast comes with a full multi-tenant organizations system out of the box. Users get a personal workspace on signup, can create additional organizations, invite teammates, and manage roles and permissions.
We leverage the organizations gem for this.
What you get out of the box
When a user signs up, they automatically get a personal organization called "My Workspace". No onboarding screen, no friction - they go straight to the dashboard. RailsFast records it as their personal workspace (user.personal_organization, organization.personal?), so closing the account can take it along.
From there, users can:
- Create additional organizations for teams or projects
- Invite teammates via email with role-based permissions
- Switch between organizations using the org switcher in the header
- Manage team members from the Settings page (invite, remove, change roles)
Organization switcher
The dashboard header includes an organization switcher dropdown (both mobile and desktop). Users can quickly switch between their organizations:

<%# Already included in the dashboard header %>
<%= render "components/railsfast/organizations/switcher" %>
The switcher shows:
- Current organization with a checkmark
- Other organizations the user belongs to
- "Create new organization" link
Team management in Settings
The Settings page includes full team management UI:

- Organization section - rename the organization
- Team Members section - view all members, invite new ones, change roles, remove members
- Plan & Billing section - subscription status and billing management (organization-level)
All team management is permission-based. Only owners and admins can invite/remove members and change roles.
Roles and permissions

RailsFast uses the default role hierarchy:
owner > admin > member > viewer
Each role inherits permissions from roles below it:
| Permission | Viewer | Member | Admin | Owner |
|---|---|---|---|---|
| View organization | ✓ | ✓ | ✓ | ✓ |
| View members | ✓ | ✓ | ✓ | ✓ |
| Create resources | ✓ | ✓ | ✓ | |
| Edit own resources | ✓ | ✓ | ✓ | |
| Delete own resources | ✓ | ✓ | ✓ | |
| Invite members | ✓ | ✓ | ||
| Remove members | ✓ | ✓ | ||
| Edit member roles | ✓ | ✓ | ||
| Manage settings | ✓ | ✓ | ||
| View billing | ✓ | ✓ | ||
| Manage billing | ✓ | |||
| Transfer ownership | ✓ | |||
| Delete organization | ✓ |
You can customize roles, or add your own, in config/initializers/organizations.rb (see Custom roles). Every role picker offers only roles at or below the signed-in person's own.
Organization-level billing
By default, RailsFast ships with billing at the organization level (not user level). This is the standard B2B SaaS pattern where:
- Organizations have subscriptions, not individual users
- All team members share the organization's plan
- Plan limits apply to the organization (e.g., team member seats)
The default checkout and billing flow is built around current_organization, so this is the path that works out of the box for standard B2B apps.
This is configured in app/models/concerns/organization_extensions.rb:
# Subscriptions are at the organization level
pay_customer default_payment_processor: :stripe
# Plan limits enforcement
include PricingPlans::PlanOwner
Team member seat limits
RailsFast automatically enforces team member limits based on the organization's plan. Configure limits in your pricing_plans.rb:
plan :pro do
limits :team_members, to: 10
# ...
end
When an organization reaches its seat limit, invitations are blocked with an upgrade message. The same check runs again when someone actually joins (on_member_joining, under a row lock), so two invitations accepted at once cannot push an organization past its limit. Keep both hooks if you edit the initializer: every join path goes through on_member_joining.
Invitation emails
Team invitations are sent using beautiful Goodmail templates. The invitation email includes:
- Who invited them
- Which organization they're joining
- A secure accept link
When the invitee clicks the link, they see a clean accept page showing the organization, who invited them, and their assigned role:

If they're not signed in, they can create an account or sign in to accept. Invitations expire after 7 days by default (configurable).
Using organizations in your code
Current organization
Access the current organization anywhere:
# In controllers
current_organization # => Organizations::Organization
current_organization.name # => "Acme Corp"
# Require organization (redirects if none)
before_action :require_organization!
Role checks
# Boolean checks
current_user.is_organization_owner?
current_user.is_organization_admin?
current_user.is_organization_member?
# Permission checks
current_user.has_organization_permission_to?(:invite_members)
current_user.has_organization_permission_to?(:manage_billing)
Scoping resources to organizations
current_organization and the organization guards come from the gem and are available in every controller. The template's own signed-in pages (/dashboard, /settings) inherit from DemoController and sit inside the authenticate :user do block in config/routes.rb, which is what requires a signed-in user. An organization-scoped controller of your own follows the same pattern:
# config/routes.rb, inside `authenticate :user do ... end`
resources :projects
# app/controllers/projects_controller.rb
class ProjectsController < DemoController
layout "dashboard"
before_action :require_organization!
def index
@projects = current_organization.projects
end
end
require_organization! redirects a user with no organization to /organizations/new (config.redirect_path_when_no_organization), and require_organization_permission_to!(:manage_settings) guards a single permission.
Delegated methods
The User model delegates common organization methods:
# These work on current_user and read its current organization
current_user.organization_name # => "Acme Corp"
current_user.current_plan # => the organization's pricing plan
current_user.pay_subscription_active? # => true/false
current_user.pay_on_trial? # => true/false
Configuration
All organization settings live in config/initializers/organizations.rb:
Organizations.configure do |config|
# Auto-create "My Workspace" on signup, recorded as the user's personal workspace
config.always_create_personal_organization_for_each_user = true
config.default_organization_name = "My Workspace"
config.record_personal_organizations = true
# Closing an account takes the personal workspace along when nobody else is in it
config.account_deletion_policy = :destroy_personal_organization
# Invitation settings
config.invitation_expiry = 7.days
config.default_invitation_role = :member
# Beautiful invitation emails
config.invitation_mailer = "OrganizationInvitationGoodmailer"
# Redirects
config.after_organization_created_redirect_path = "/dashboard"
config.redirect_path_after_invitation_accepted = "/dashboard"
# Enforce seat limits, when someone joins (every join path) and when someone is invited
config.on_member_joining do |ctx|
org = ctx.organization
org.lock!
limit = org.current_plan&.limit_for(:team_members)
if limit && org.member_count >= limit
raise Organizations::MembershipVetoed, "You've reached your plan's team member limit (#{limit})."
end
end
config.on_member_invited do |ctx|
org = ctx.organization
limit = org.current_plan&.limit_for(:team_members)
if limit && org.member_count >= limit
raise Organizations::InvitationError, "You've reached your plan's team member limit (#{limit})."
end
end
end
Architecture patterns: User-based vs Organization-based
Before diving into customization, it's worth understanding when resources should belong to organizations vs users. This decision affects your entire data model.
There are really three separate questions here:
- Who owns the resource?
- Who pays for access?
- Are organizations required, optional, or just a reporting layer?
Don't collapse those into one decision. In many apps, the right answer is:
- resources belong to users
- organizations are optional
- billing may exist at the organization level, user level, or both
The key question
Ask yourself: "Who owns this resource semantically?"
Consider a fitness tracking app where users log workouts. If a user leaves their gym, do their workouts stay with the gym or go with the user? Obviously the user: workouts are personal data, not gym assets.
Now consider a project management tool. If an employee leaves the company, do their projects stay with the company or go with them? The company: projects are corporate assets.
This semantic ownership should drive your architecture.
Don't add organization_id to personal-domain records just because you might want B2B later. If the resource is fundamentally personal, keep it user-owned and let organizations provide access, reporting, or billing on top.
Pattern 1: Organization-based (RailsFast default)
Resources belong to organizations. Billing is at the organization level. This is the standard B2B SaaS pattern.
# Resources belong to organizations
class Project < ApplicationRecord
belongs_to :organization
end
# Organization has billing (from organization_extensions.rb)
# pay_customer default_payment_processor: :stripe
Use this when:
- Companies are your customers
- Teams share resources (projects, documents, API keys)
- Resources should stay with the company when employees leave
- Billing is per-team
Examples: Project management, CRM, analytics dashboards, API platforms
Pattern 2: User-owned resources with optional organizations
Resources belong to users. Organizations exist for grouping, social features, or B2B dashboards, but they don't own the resources.
Real-world example: A fitness app with gym memberships
Imagine building a workout tracking app where:
- Users log workouts, earn achievements, and track calories and training time
- Users can join gyms (organizations) to participate in challenges and see leaderboards
- Gyms pay a subscription for dashboards showing member activity and engagement stats
- When a user leaves a gym, their workout history goes with them
Here's how to model this:
# Workouts belong to USERS, not organizations
class Workout < ApplicationRecord
belongs_to :user
# Workout data
attribute :exercise_type, :string
attribute :duration_minutes, :integer
attribute :calories_burned, :integer
attribute :completed_at, :datetime
end
class User < ApplicationRecord
has_organizations # Can join gyms
has_many :workouts
# User's personal stats
def total_workouts
workouts.count
end
def calories_this_month
workouts.where(completed_at: Time.current.beginning_of_month..).sum(:calories_burned)
end
end
The key insight: Users can belong to multiple gyms, but their workouts don't "belong" to any gym. The gym gets a reporting view into member activity, not ownership.
Implementing the gym dashboard
Gyms need to see aggregated member stats. Add reporting methods to Organization via organization_extensions.rb:
# app/models/concerns/organization_extensions.rb
module OrganizationExtensions
extend ActiveSupport::Concern
included do
# Gym pays for dashboard access
pay_customer default_payment_processor: :stripe
include PricingPlans::PlanOwner
end
# === Member Activity Reporting ===
# These are READ-ONLY views into member data, not ownership
def member_workouts(period: nil)
scope = Workout.where(user_id: users.select(:id))
scope = scope.where(completed_at: period) if period
scope
end
def gym_stats(period: nil)
workouts = member_workouts(period: period)
{
total_workouts: workouts.count,
total_calories: workouts.sum(:calories_burned),
total_minutes: workouts.sum(:duration_minutes),
active_members: workouts.distinct.count(:user_id),
avg_workouts_per_member: member_count.positive? ? (workouts.count.to_f / member_count).round(1) : 0
}
end
def leaderboard(period: nil, limit: 10)
Workout
.where(user_id: users.select(:id))
.where(period ? { completed_at: period } : {})
.group(:user_id)
.select("user_id, COUNT(*) as workout_count, SUM(calories_burned) as total_calories")
.order("workout_count DESC")
.limit(limit)
end
def member_engagement_report(period:)
active_ids = member_workouts(period: period).distinct.pluck(:user_id)
{
total_members: member_count,
active_members: active_ids.count,
inactive_members: member_count - active_ids.count,
engagement_rate: member_count.positive? ? ((active_ids.count.to_f / member_count) * 100).round(1) : 0
}
end
end
Configuration for User-based apps
If organizations are optional in your app, turn off the personal-workspace default:
# config/initializers/organizations.rb
Organizations.configure do |config|
# Users can exist without an organization
config.always_create_personal_organization_for_each_user = false
config.always_require_users_to_belong_to_one_organization = false
# ... rest of config
end
This changes the organization behavior, not your whole billing architecture.
Supporting both individual and organization billing
If you want individual users and organizations to have subscriptions:
class User < ApplicationRecord
has_organizations
has_many :workouts
# Individual PRO billing primitive
pay_customer default_payment_processor: :stripe
end
# Organizations also have subscriptions (dashboard access, analytics, etc.)
# This is already in organization_extensions.rb:
# pay_customer default_payment_processor: :stripe
Now you have two billing paths:
- Individual PRO: User pays for premium personal features
- Organization subscription: Organization pays for reporting, admin tools, team access, etc.
Adding pay_customer to User is only the billing primitive. RailsFast's default checkout, billing portal, pricing UI, and common subscription delegates are still organization-scoped. If you want real dual billing, you will usually need:
- a separate checkout / billing flow for
User - a clear entitlement layer for feature access
- to adjust the default
Userdelegates that pointcurrent_plan,pay_subscription_active?and friends atcurrent_organization - to decide whether
Usershould also become aPricingPlans::PlanOwner, instead of assuming that drops in cleanly - discipline around not inferring resource ownership from whoever pays
When to use each pattern
| Question | Organization-based | User-based |
|---|---|---|
| Who's the primary customer? | Companies | Individuals |
| Do resources stay when user leaves? | Yes (company assets) | No (personal data) |
| Is billing per-team or per-user? | Per-team | Per-user (or optional org billing) |
| Are resources shared or personal? | Shared | Personal |
| Example resources | Projects, API keys, team docs | Workouts, recipes, transactions |
The hybrid approach works
The gym example shows you can have both:
- B2C core: Users own their workout history and can use the app solo
- B2B layer: Organizations pay for dashboards showing member activity
This is often the right architecture for:
- Fitness apps with gyms or corporate wellness programs
- Expense trackers with company reporting
- Learning platforms with team/school dashboards
- Any B2C app that might add B2B features later
The organizations gem supports both patterns equally well. The key is deciding upfront:
- Do organizations own resources?
- Or do they provide a lens into user-owned resources?
- And separately, who pays for access?
Danger Zone: Transfer, Delete & Leave
The organization settings page includes a Danger Zone section at the bottom. What users see depends on their role:
- Owners see Transfer Ownership and Delete Organization options
- Non-owners see a Leave Organization option
These are also the tools for closing an account: an organization can't exist without an owner, so any organization a user owns, other than a personal workspace they are alone in, must be handed over or deleted before the account can go.

Transfer ownership
Owners can transfer ownership to any admin in the organization. The transfer flow:
- Click "Transfer" in the Danger Zone
- Select an eligible admin from the list
- Type the organization name to confirm
- The previous owner becomes an admin
Only admins can receive ownership. If you want to transfer to a member, promote them to admin first using the Team Members section.
Handing over a personal workspace makes it a shared organization of the new owner. The confirmation step says so before you confirm.
Delete organization
Owners can permanently delete an organization:
- Click "Delete" in the Danger Zone
- Type the organization name to confirm
- All data is permanently deleted
Both actions use type-to-confirm modals to prevent accidents.
Account closure flow
Accounts close in Settings → Danger Zone, behind the account's password. Before anyone confirms, the page shows what will happen, from the gem's deletion plan (user.account_deletion_plan):
| The user owns… | What happens |
|---|---|
| nothing | the account is deleted |
| their personal workspace, as its only member | the account and the workspace are deleted together |
| their personal workspace, with other members | blocked until they hand it over or remove the members |
| another organization, as its only member | blocked until they delete it |
| another organization, with other members | blocked until they transfer it |
A blocked closure lists each organization with what to do, and a link to it. The check and the deletion run in one database transaction, under row locks, so a refusal changes nothing: no account is half-deleted and no organization is left without an owner. The user's other memberships end with the account. Deleting a workspace with a subscription cancels the subscription.
Devise's own DELETE /users deletes nothing in RailsFast: it sends the user to the Danger Zone. An account created with "Continue with Google" has no password of its own, so it is asked to set one first.
Leave organization (for non-owners)
Non-owner members see a Leave Organization panel instead of the Danger Zone. This allows them to remove themselves from an organization they no longer want to be part of.
The leave action:
- Removes the user's membership from the organization
- Redirects to the organizations list
- Auto-switches to another organization if available
Owners cannot leave their organization. They must transfer ownership first, or delete the organization entirely.
Hybrid onboarding (invited vs direct signups)
By default, RailsFast creates a "My Workspace" for every new user. But when users sign up via invitation to join an existing organization, creating a personal workspace is unnecessary clutter.
RailsFast implements hybrid onboarding out of the box:
- Direct signups → Get "My Workspace" immediately
- Invited signups → Skip personal workspace, join the invited org directly
This is handled automatically via a flag in the User model and Devise's build_resource hook. You don't need to configure anything.
How it works
The User model overrides the gem's should_create_personal_organization? predicate:
# app/models/user.rb
class User < ApplicationRecord
has_organizations
# Skip personal org creation for invited signups
attr_accessor :skip_personal_organization
def should_create_personal_organization?
return false if skip_personal_organization
super
end
end
The registrations controller sets the flag before user creation:
# app/controllers/users/registrations_controller.rb
def build_resource(hash = {})
super
if pending_organization_invitation?
resource.skip_personal_organization = true
end
end
This ensures the flag is set before the user is persisted, so the gem's after_create callback respects it.
Customization options
Users with no organization (B2C / consumer apps)
By default, RailsFast auto-creates a "My Workspace" for every new user. If you want users to have no organization by default (e.g., for B2C consumer apps, or if organizations are optional), just change one line in the initializer:
# config/initializers/organizations.rb
config.always_create_personal_organization_for_each_user = false
That's it for the organizations behavior. New users will sign up without an organization. They can create one later if needed.
If you're also using RailsFast's default org-level billing flow, you may still want to adapt the subscription screens and controllers so they don't assume current_organization.
The User model's has_organizations block inherits from the initializer settings. You only need to use the block if you want to override the global config for a specific model. For most apps, configure everything in the initializer and use a simple has_organizations without a block.
Custom onboarding flow
If you want users to go through an onboarding screen to create their organization:
# config/initializers/organizations.rb
config.always_create_personal_organization_for_each_user = false
config.redirect_path_when_no_organization = "/onboarding/create_organization"
The template ships no onboarding screen: build the controller and view at that path yourself. Until you do, leave the default /organizations/new, the gem's own "create an organization" page.
Add custom organization fields
To add fields like logo or billing_email:
- Add a migration:
add_column :organizations_organizations, :logo, :string
add_column :organizations_organizations, :billing_email, :string
- Permit the params:
# config/initializers/organizations.rb
config.additional_organization_params = [:logo, :billing_email]
- Update the organization form partial.
Custom roles
Add a role beside owner, admin, member and viewer, with its own permissions plus its parent's. A block that only adds custom roles keeps the built-in roles' defaults:
# config/initializers/organizations.rb
config.roles do
role :client, inherits: :viewer do
can :answer_questions
end
end
Name it in the UI with organizations.roles.client in your locale files. The invitation form, the members page and the member dropdown list it with no view changes, and a custom role can be assigned as soon as it is configured.
The gem checks two rules at boot: a custom role inherits from member, viewer or another custom role (never admin or owner), so it ranks below admin; and it cannot hold a permission only the owner has (manage_billing, transfer_ownership, delete_organization). Since organizations 0.6.1, a role also acts only on roles at or below its own (invitations, role changes, removals, invitation revoke and resend), and a role below admin must hold every permission of the role it acts on. The models enforce this, so a hand-crafted request is refused just like the picker would refuse it. add_member!, approve_join_request! and direct Membership writes take no actor: authorize those yourself.
organizations 0.6.0 (RailsFast v1.4.0) did not have that rule yet: a custom role with invite_members or edit_member_roles could craft a request that invited an admin or promoted someone to admin, and an admin invitation created that way still grants admin when it is accepted later. If you ran custom roles with those permissions on v1.4.0, audit pending invitations and recent role changes made by members of those roles.
There is one config.roles block. A block that redefines a built-in role defines only the built-in roles it lists, so if you change one, list all four.
Rename organizations (vocabularies)
Call organizations teams, workspaces or firms without touching a view. Name a vocabulary and write only the words that differ, in a locale file your app owns:
# config/initializers/organizations.rb
config.vocabulary = :team
# config/locales/en/vocabulary.yml
en:
organizations:
vocabularies:
team:
roles:
admin: Team lead
organizations:
index:
title: Teams # organizations.organizations.index.title
demo:
settings:
show:
organization_heading: Team # demo.settings.show.organization_heading
The vocabulary key is the ordinary key after organizations.vocabularies.<name>., without a leading organizations.. A key the vocabulary lacks keeps its ordinary words. The organization pages and components, the organization parts of Settings, the invitation email and every role label read it, through organization_t (t with the vocabulary in front) and organization_role_label(role, organization:). Use them in your own views too. One vocabulary per organization works with a lambda: config.vocabulary = ->(organization) { organization.metadata["vocabulary"] }.
Scoped resource grants and domain-based joining
Both are off by default and one generator away: grants limit a member to some records (rails g organizations:grants), and the join screens admit people by a verified email domain, a join code or an allowlist (rails g organizations:join). The template's docs/organizations.md has the setup, the configuration keys and the caveats.
Upgrading from organizations 0.6
RailsFast v1.5.0 moves to organizations 0.7. After git merge, the short version is:
bundle install, thenbin/rails db:migrate. Three template migrations add the personal-workspace link, the domain verification columns, and a fix that lets the database delete a user who once invited someone. All three are safe for strong_migrations on a busy table.bin/rails generate organizations:upgrademust print that organizations is up to date. Run it only after the migrations: before, it writes the gem's own versions, which strong_migrations refuses.- If you resolved a conflict in
config/initializers/organizations.rb, it must keeprecord_personal_organizations = true,account_deletion_policy = :destroy_personal_organizationandhelper OrganizationsHelperin theon_load(:organizations_public_controller)block. Keep the template's new locale keys (bundle exec i18n-tasks missinglists any the merge dropped), and keep theast_matcherslines if you customisedconfig/i18n-tasks.yml. - Link the workspaces of accounts created before the upgrade with the reviewed console backfill in the template's
docs/organizations.md. Until then those accounts behave as before.
The same file covers the behaviour changes that can reach your own code, and what to check if you customised the registrations controller, the settings controller or a danger zone.
Admin panel
Organizations appear in the Madmin admin panel at /admin/dashboard/organizations. You can view:
- All organizations with member counts
- Subscription status and plan, and whose personal workspace an organization is
- Scopes for filtering (active subscribers, on trial, churned, at or over plan limits, etc.)
Key files
| File | Purpose |
|---|---|
config/initializers/organizations.rb |
Main configuration |
app/models/concerns/organization_extensions.rb |
Pay + PricingPlans integration |
app/models/user.rb |
has_organizations + hybrid onboarding |
app/controllers/users/registrations_controller.rb |
Invitation-aware signup; DELETE /users points to Settings |
app/controllers/demo/settings_controller.rb |
Settings, including account closure |
docs/organizations.md |
Personal workspaces, account closure, vocabularies, grants, upgrading |
app/controllers/demo_controller.rb |
Base controller for org-scoped pages |
app/helpers/organizations_helper.rb |
Role badges, role pickers, organization_t |
app/mailers/organization_invitation_goodmailer.rb |
Invitation emails |
app/views/components/railsfast/organizations/ |
UI components (switcher, team list, danger zone) |
app/views/demo/settings/ |
Settings page partials |
app/javascript/controllers/railsfast/organizations/transfer_modal_controller.js |
Transfer ownership modal |
app/javascript/controllers/railsfast/organizations/confirm_modal_controller.js |
Delete confirmation modal |
The organizations gem has many more features! Check out the gem docs for advanced usage like ownership transfer, custom callbacks, Acts As Tenant integration, and more.