# How the Family/Family Member System Enables Multi-User Support in Maybe Finance

> Discover how Maybe Finance's family and user system enables secure multi-user support. Learn about their single-tenant model for household data isolation and access control.

- Repository: [Maybe/maybe](https://github.com/maybe-finance/maybe)
- Tags: internals
- Published: 2026-03-07

---

**Maybe Finance implements multi-user support through a single-tenant "family" model where a `Family` record acts as the isolation boundary for household data, while individual `User` records belong to one family and the `Current` thread-local object maintains request-scoped access control.**

The Maybe Finance platform is architected around families rather than individual users to support collaborative household financial management. This family-centric design allows multiple members to share accounts and budgets while ensuring strict data segregation between different households. Understanding how the family and family member system works reveals the robust access control patterns that keep financial data secure in this open-source Rails application.

## Core Data Model

The multi-user architecture rests on three interconnected models that establish ownership and access patterns throughout the application.

### The Family Model

The `Family` model ([`app/models/family.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/family.rb)) serves as the root tenant container. It owns all household financial data through `has_many` associations:

```ruby

# app/models/family.rb

has_many :users, dependent: :destroy
has_many :accounts, dependent: :destroy
has_many :transactions, through: :accounts

```

This design means that deleting a family cascades to all associated records, making it the central isolation boundary.

### The User Model

The `User` model ([`app/models/user.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/user.rb)) represents individual family members who log into the application. Each user belongs to exactly one family:

```ruby

# app/models/user.rb

belongs_to :family
enum role: { member: "member", admin: "admin", super_admin: "super_admin" }

```

Users carry a role enum that determines their permissions within the household. The model provides helper methods to check privileges:

```ruby

# app/models/user.rb#L23-L27

def admin?
  super_admin? || role == "admin"
end

```

### The Current Attributes Pattern

Maybe Finance uses Rails' `ActiveSupport::CurrentAttributes` to maintain thread-safe access to the authenticated context:

```ruby

# app/models/current.rb

class Current < ActiveSupport::CurrentAttributes
  attribute :user, :family
end

```

During each request, `Current.user` holds the authenticated user and `Current.family` holds their associated family. This pattern appears throughout controllers and services to ensure all data access is properly scoped.

## Role-Based Access Control

The application implements three distinct permission levels through the `role` enum defined in [`app/models/user.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/user.rb):

- **member**: Standard access to read and create data within the family
- **admin**: Can invite users, export data, and manage family settings
- **super_admin**: Full system access including administrative functions

Controllers check these roles using the `admin?` helper. For example, in `FamilyExportsController` and `Settings::ProfilesController`, privileged actions verify `Current.user.admin?` before proceeding.

## Request Scoping and Data Isolation

All domain-level queries automatically scope to the current family through `Current.family`. This pattern ensures that users can never access data belonging to other households.

In `app/controllers/transactions_controller.rb#L8-L14`, the controller loads data exclusively for the authenticated user's family:

```ruby

# app/controllers/transactions_controller.rb#L8-L14

@income_categories = Current.family.categories.incomes.alphabetically
@search = Transaction::Search.new(Current.family, filters: @q)

```

Because `Current.family` derives from `Current.user`, the authentication layer in `app/controllers/concerns/authentication.rb#L51-L62` sets both values after successful login, establishing the security context for the entire request lifecycle.

## Multi-User Workflows

### Inviting New Members

Admins can invite additional family members through the `InvitationsController`. The invitation record belongs to the family, ensuring that accepted invites automatically associate new users with the correct household:

```ruby

# In a controller action (admin only)

def invite_member
  invitation = Current.family.invitations.create!(
    email: params[:email],
    inviter: Current.user
  )
  InvitationMailer.with(invite: invitation).invite_email.deliver_later
end

```

*Source:* `app/controllers/invitations_controller.rb#L14-L15`

### Authentication Flow

When a user logs in, the authentication concern sets the thread-local context:

```ruby

# app/controllers/concerns/authentication.rb#L51-L62

def set_current_user
  Current.user = user
  Current.family = user.family
end

```

This ensures that every subsequent database query, background job enqueue, or service object instantiation has access to the correct tenant context.

## Account Deactivation and Cleanup

Maybe Finance implements safeguards to prevent orphaned families or data loss when users leave. The `User#deactivate` method in `app/models/user.rb#L65-L70` validates that an admin cannot deactivate their account if they are the last admin in a family with other members:

```ruby

# app/models/user.rb#L65-L70

def can_deactivate
  errors.add(:base, :cannot_deactivate_admin_with_other_users) if admin? && family.users.count > 1
end

```

If the user is the last member of the family, the deactivation process also destroys the entire `Family` record and all associated data, ensuring clean data removal.

## Practical Implementation Examples

### Scoping Queries to the Current Family

Access family-specific data through `Current.family` in controllers or services:

```ruby

# List all budgets for the signed-in user's family

budgets = Current.family.budgets.order(:name)

```

*Source:* `app/models/family.rb#L33-L34`

### Enforcing Admin Privileges

Restrict destructive actions to administrators:

```ruby
def destroy
  unless Current.user.admin?
    redirect_to root_path, alert: "Only admins can delete families."
    return
  end

  Current.family.destroy
  redirect_to root_path, notice: "Family deleted."
end

```

*Source:* `app/models/user.rb#L65-L67`

### Background Job Isolation

When enqueueing background jobs, pass the family ID to maintain scoping outside the request cycle:

```ruby
class FamilyDataExportJob < ApplicationJob
  def perform(family_id)
    family = Family.find(family_id)
    # Export logic scoped to this family only

    Family::DataExporter.new(family).export
  end
end

```

The controller enqueues the job with `Current.family.id`, ensuring the export contains only the requesting household's data.

*Source:* `app/controllers/family_exports_controller.rb#L12-L24`

## Summary

- **Single-tenant architecture**: Each `Family` record acts as an isolation boundary for household financial data.
- **User membership**: `User` records belong to one family via `belongs_to :family`, with role-based permissions (member, admin, super_admin).
- **Thread-local scoping**: The `Current` attributes class maintains `Current.user` and `Current.family` throughout the request lifecycle.
- **Automatic isolation**: All queries use `Current.family` to ensure data never leaks between households.
- **Invitation workflow**: New users join families through `Invitation` records that preserve the family association.
- **Safe cleanup**: Deactivation logic prevents removing the last admin without transferring ownership and cleans up empty families.

## Frequently Asked Questions

### What is the difference between a Family and a User in Maybe Finance?

A `Family` is the root tenant container that owns all household data including accounts, transactions, and budgets. A `User` represents an individual login account that belongs to exactly one family. While multiple users can belong to the same family, each user can only access data within their assigned family.

### How does Maybe Finance ensure data isolation between different households?

The application uses the `Current` thread-local object to store the authenticated `user` and their `family` during each request. All database queries scope to `Current.family`, as seen in controllers like `TransactionsController`, ensuring that SQL queries filter records by the user's family ID. This architectural pattern prevents cross-family data leakage by design.

### What roles are available for family members?

Maybe Finance defines three roles in the `User` model enum: **member** (standard read/write access), **admin** (can invite users, export data, and manage settings), and **super_admin** (full system privileges). The `admin?` method returns true for both admins and super_admins, while specific super-admin checks use `super_admin?`.

### How does the invitation system work for adding new family members?

Admins create `Invitation` records associated with `Current.family`. When an invitee accepts through the sign-up link, the application creates a new `User` record automatically associated with `invitation.family`. This workflow ensures new members join the correct household without manual family ID assignment.