Add Sign Up to Rails 8' Authentication

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.

Published at . Have suggestions or improvements on this content? Do reach out.