This is the first article in a completely new section on Rails Designer: Build a SaaS with Ruby on Rails. It will be a collection of pragmatic, practical articles with tips, insights and best practices on building a SaaS with Rails in 2024 and beyond.
Rails 8 ships (finally!) with authentication out-of-the-box. ❤️ When it launched most were surprised it didn’t come with a way to register (sign up) a new user. But this was a conscious decision, because creating a User isn’t typically the only resource needed for a SaaS. There are many other actions that need to be taken and resources created when someone new signs up for your product.
This article explores the way I do it using something, typically referred to as a Form Object. Let’s check it out.
If you want to follow along, check out this repo. This given commit is a vanilla Rails 8 app where bin/rails generate authentication
has been run.
For new features, I always like to start from the outside in. This means I start with the UI and then work my way into the internals (more on that in another article).
Let’s create the form at app/views/signups/new.html.erb:
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
<%= form_with model: @signup do |form| %>
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
<%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
<%= form.submit "Sign up" %>
<% end %>
It’s pretty similar to Rails’ sessions/new.html.erb.
Note: articles in this section focus on the (business) logic and come without any styling. I suggest to check out the vast collection of articles on Rails Designer to improve your UI, CSS and JavaScript skills! 🧑🎓 🎨
Next up the route:
# config/routes.rb
root to: "pages#show"
resource :signups, path: "signup", only: %w[new create]
I also already added a root route, where the user will be redirected to upon successful sign up.
Then the controller. It is pretty straight-forward:
class SignupsController < ApplicationController
allow_unauthenticated_access only: %w[new create]
def new
@signup = Signup.new
end
def create
@signup = Signup.new(signup_params)
if user = @signup.save
start_new_session_for user
redirect_to root_url
else
redirect_to new_signups_path
end
end
private
def signup_params
params.expect(signup: [ :email_address, :password ])
end
end
allow_unauthenticated_access
is coming from Rails’ authentication generator. Then another little new thing is params.expect(signup: [ :email_address, :password, :terms ])
. Previously you might have seen params.require(:signup).permit(:email_address, :password)
. It’s a new Rails 8+ syntax, added in this PR.
It all looks pretty familiar, right? But the special bit is in the Signup class. This won’t be your regular ActiveModel, but will be a so called Form Object.
# app/models/signup.rb
class Signup
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email_address, :string
attribute :password, :string
validates :email_address, presence: true
validates :password, length: 8..128
def save
if valid?
User.create!(email_address: email_address, password: password)
end
end
def model_name
ActiveModel::Name.new(self, nil, self.class.name)
end
end
You see I just store it in app/models; no other folder needed. Also note it’s just a vanilla Ruby class (often referred to as PORO), ie. it doesn’t inherit from ApplicationRecord, but it looks (and quacks!) pretty much like one.
That is due to the model_name
method and the two modules included. This allows you to reference the object in the form builder (form_with model: Signup.new
).
To me the beauty of this set up is you now have a class that can be responsible for much more. After all, when signing up for a new SaaS often many more things happen:
- create a workspace
- create a team
- send welcome email
- create sign up event
- and so on…
Whatever else is needed for your specific use case. Let’s extend above class a bit with an example:
# app/models/signup.rb
class Signup
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email_address, :string
attribute :password, :string
validates :email_address, presence: true
validates :password, length: 8..128
def save
if valid?
User.create!(email_address: email_address, password: password).tap do |user|
create_workspace_for user
send_welcome_email_to user
end
end
end
def model_name
ActiveModel::Name.new(self, nil, self.class.name)
end
private
def create_workspace_for(user)
# eg. user.workspaces.create
end
def send_welcome_email_to(user)
# eg. WelcomeEmail.perform_later user
end
end
Now when valid?
returns true, a workspace is created and a welcome email is sent. It also uses tap
: this will pass along the user object and returns it as an object well (which is useful in the SignupsController#create action.
I like to write my methods to be easy to read and understand (similar to how the Rails’ 8 authentication code is written, e.g. start_new_session_for user
).
I have now only added two extra steps, but you can imagine that Workspace creation would happen in another class again too. Feel free to extend with whatever you need.
Let’s not stop here, let’s add a checkbox for the user to accept your Terms of Service. First update the view:
<%= form_with model: @signup do |form| %>
<%# … %>
<%= form.check_box :terms %>
<%= form.label :terms %><br>
<% end %>
Then validate it is present in the Signup class.
class Signup
# …
attribute :terms, :boolean, default: false
validates :terms, acceptance: true
# …
end
Then allow the attribute to be passed along in the params.
class SignupsController < ApplicationController
# …
def signup_params
params.expect(signup: [ :email_address, :password, :terms ])
end
end
Similarly you can add a checkbox to opt them in for your email newsletter (and store as a preference using something like a vault as described in this article! 💡
To finalize this from start to finish, let’s create a simple view where the user is redirected to upon sign up.
# app/views/pages/show.html.erb
<p>Sign up Successful</p>
<%= button_to "Log out", session_path, method: :delete %>
Using this approach you have a class that can contain all required steps for sign ups. Easy to reason about, and easy to test!
Of course this is just the start! From UI and CSS (check out Rails Designer’s UI Components Library) to adding validation messages and so on.
And with that you have added sign ups to your Rails 8 authentication. Also this code is not limited to Rails 8, and can be as easily added to older versions of Rails.