DOCS LLMs

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:

Organization switcher

<%# 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:

Team members list

  • 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

Member 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:

Accept invitation page

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:

  1. Who owns the resource?
  2. Who pays for access?
  3. 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.

WARNING

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.
IMPORTANT

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 User delegates that point current_plan, pay_subscription_active? and friends at current_organization
  • to decide whether User should also become a PricingPlans::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.

Danger Zone

Transfer ownership

Owners can transfer ownership to any admin in the organization. The transfer flow:

  1. Click "Transfer" in the Danger Zone
  2. Select an eligible admin from the list
  3. Type the organization name to confirm
  4. The previous owner becomes an admin
NOTE

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:

  1. Click "Delete" in the Danger Zone
  2. Type the organization name to confirm
  3. 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:

  1. Removes the user's membership from the organization
  2. Redirects to the organizations list
  3. Auto-switches to another organization if available
NOTE

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.

NOTE

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:

  1. Add a migration:
add_column :organizations_organizations, :logo, :string
add_column :organizations_organizations, :billing_email, :string
  1. Permit the params:
# config/initializers/organizations.rb
config.additional_organization_params = [:logo, :billing_email]
  1. 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.

CAUTION

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.

WARNING

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:

  1. bundle install, then bin/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.
  2. bin/rails generate organizations:upgrade must 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.
  3. If you resolved a conflict in config/initializers/organizations.rb, it must keep record_personal_organizations = true, account_deletion_policy = :destroy_personal_organization and helper OrganizationsHelper in the on_load(:organizations_public_controller) block. Keep the template's new locale keys (bundle exec i18n-tasks missing lists any the merge dropped), and keep the ast_matchers lines if you customised config/i18n-tasks.yml.
  4. 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

INFO

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.