04 Feb 2020
This is a blog post goes through some of the best practices that I have found out over the years when I have been building SaaS’ish web applications.
I use Devise as my authentication library. I consider it the de facto authentication solution, at least until Ruby on Rails introduces an authentication solution that is built into Ruby on Rails.
I have been thinking about this subject for months, but this tweet by Ben Orenstein was the final push to get me into writing mode.
“Has anyone written something great about how to model your Rails-based SaaS app with Users, Teams, Organizations, and Subscriptions so that you don’t regret it later?”
The Organization
is a model that is at the core of everything. Every User
will be a member of an organization through an association called Membership
.
When a user is created by Devise, it creates an organization and membership and associates all of these three together.
The day will come when you need to associate your users in organizations and it will be the refactor from your worst nightmares. You will be better off by just associating organizations and users from the beginnings of your project.
I like to use two different controllers for guest users and logged in users, ApplicationController
is used for users who are not signed in and SignedInApplicationController
is used when I want to restrict the controller only for users who are currently signed in.
ApplicationController is as plain as possible.
# app/controller/application_controller.rb
class ApplicationController < ActionController::Base
protected
def after_sign_in_path_for(user)
designs_path(user)
end
end
SignedInApplicationController
has a larger set of responsibilities. It exposes two helper-methods, current_user
is a Devise method and current_organization
is defined here. It also forces to use a specific layout for signed-in users.
# app/controller/signed_in_application_controller.rb
class SignedInApplicationController < ActionController::Base
before_action :authenticate_user!
layout "signed_in_application"
helper_method :current_organization
helper_method :current_user
protected
def current_organization
@current_organization ||= current_user.organization
end
end
I like to put models and controllers into namespaces when if they share similar responsibilities. Authentication and accounts prime candidates for namespacing and putting the files into subfolders.
As I mentioned, I am using Devise as go-to my authentication library. It works perfectly and does everything I need. I just override the create-method in registration controller, because I want to create Organization
and Membership
at the same time when User
is created.
Directory structure
controllers
app/controllers/authentication/registrations_controller.rb
models
app/models/accounts/user.rb
app/models/accounts/organization.rb
app/models/accounts/membership.rb
The connection between User
and Organization
uses a conditional scope that returns only active memberships. This is a safety mechanism built into has_many
association method organizations
to allow access only if membership is active.
# app/models/accounts/user.rb
class Accounts::User < ApplicationRecord
devise :database_authenticatable, :registerable, :recoverable,
:rememberable, :validatable, :confirmable, :trackable
has_many :memberships
has_many :organizations, -> {where(accounts_memberships: { status: 0})}, through: :memberships
has_many :all_organizations, through: :memberships, source: :organization
def organization
# for now, just assume that user has membership with
# only one organization
organizations.first
end
end
There are a couple of ways to achieve this. I like to create the organization-model in the registrations controller.
The code here below is copied from my latest project. If you want to use this, please do not copy it from here. Instead, go to Devise repository , find the registrations_controller.rb
file and copy it from there. The create_user_organization_and_membership
method does not necessarily need to be in the controller. You can put it into a separate file and make it as a model or a service object, whatever feels right for you.
# app/controllers/authentication/registrations_controller.rb
class Authentication::RegistrationsController < Devise::RegistrationsController
def create
build_resource(sign_up_params)
# here is where the user-model is created
create_user_organization_and_membership(resource)
yield resource if block_given?
if resource.persisted?
if resource.active_for_authentication?
set_flash_message! :notice, :signed_up
sign_up(resource_name, resource)
respond_with resource, location: after_sign_up_path_for(resource)
else
set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
expire_data_after_sign_in!
respond_with resource, location: after_inactive_sign_up_path_for(resource)
end
else
clean_up_passwords resource
set_minimum_password_length
respond_with resource
end
end
protected
def after_inactive_sign_up_path_for(user)
pending_confirmation_path
end
def create_user_organization_and_membership(user)
return false unless user.valid?
ActiveRecord::Base.transaction do
user.save
organization = Accounts::Organization.create(status: "active", name: user.email, created_by: user.email)
membership = Accounts::Membership.create(organization: organization, user: user, role: "owner")
end
user
rescue ActiveRecord::RecordInvalid => e
# would be good idea to log the error message
user
end
end
When thinking about responsibilities in this architecture, the responsibility of holding the information if somebody has paid or not belongs to Organization
.
The Subscribeable
concern has the logic to determine if an organization has a payment subscription or not. It also has the functionality to start and end a subscription.
In my case, it contains free-trial functionality and requires a subscription after the free trial ends. Requiring the credit card in the registration process is a bit tricky in the EU with strong-customer-authentication (SCA) regulation.
I like to put payments into their own namespace. All related code lives in payments
namespace. Payment processor related code lives in their own directories, for example app/models/payments/processors/stripe_processor.rb
Organization
-class includes Subscribeable
concern.
# app/models/accounts/organization.rb
class Accounts::Organization < ApplicationRecord
include Payments::Subscribeable
...
end
When creating something larger than “Hello World”, things get complicated quickly. The term “it depends” is used more and more. Any blog post written about software architecture can be read with a grain of salt. So pleas remember, this is just one way of architecting a SaaS user management.
If this approach feels a bit too limited for you, head over to FireHydrant blog and read this blog post by Bobby Tables.
I would greatly appreciate it if you kindly give me some feedback on blog post!
If you have ideas on how to handle the generic-part of the architecture of a SaaS, please send me an email or tweet at me. I’m always looking to improve my skills and knowledge!
Any kind of feedback is appreciated! My email is