Creating A Separate Email Address Table with Devise in Rails

Brandon Walsh
Moonfarmer
Published in
5 min readAug 17, 2020

--

Devise is a common tool used to streamline authentication in Ruby on Rails. Its bootstrapping of Session and User models makes it very quick and easy to get a login & sign up flow working almost out of the box. However, no tool comes without its limitations. Fortunately, as developers we have the capacity to find workarounds — coding is about finding creative solutions to challenging problems. After all, it’s not the tool, it’s how you use it.

For most applications, the standard approach for having an email and password field on a User model suffices (this is also what Devise generally expects). However, things start to get messy when the User model stores email addresses in a separate table. It can get even messier if you have a tertiary model, like a Contact model, in between the User and EmailAddress.

Devise doesn’t offer any help for this and other solutions/gems are either half implemented or outdated. Today we’ll look into implementing a User model in Ruby on Rails with email addresses stored in a separate model, EmailAddress.

Note: The below code samples work with the concept of Users having many email addresses, but can easily be modified by replacing has_many: email_addresses with has_one :email_address and changing references from “email_addresses” to “email_address”. The samples also assume that your working user model is “User”. If your User model is named something other, you will also need to change your references accordingly.

Our base EmailAddress model will have the following columns:

  • user_id: int
  • email: string

Overriding Devise Methods in Our User Model

Devise adds various methods to the User model, or the model you designate, to be able to manage authentication. We’ll need to override some of these methods so devise knows not to look at the User model for an email field. We want to make our User model look like so:

The first two methods, email_required? and will_save_change_to_email?, tell Devise that when looking for or updating an “email” attribute, don’t attempt to look for or save to an “email” field on “users” table.

The last method, find_first_by_auth_conditions, is a class method that’s used to look for a User based on given conditions (usually from the sign in form). Let’s break it down:

conditions = warden_conditions.dup

Copies the form conditions into a variable, conditions.

if email = conditions.delete(:email)

If “email” was a field/attribute in the given form, remove the field from the conditions, but first store its value in a variable, email.

# If the above condition is true, perform the search query: where(conditions.to_h).includes(:email_addresses)
.where(email_addresses: {email: email}).first

This searches the “users” table by the given form condition (now without checking for an “email” field in the “users” table). Since I have an EmailAddress model (with an “email” field), we tell the query to load associations from the User model with the “email_addresses” table in advance with .includes(:email_addresses). Finally, with where(email_addresses: { email: email }).first, we search the “email_addresses” table for the provided email, and we get 1 user who has an association with the provided email address, if such a user exists.

else
where(conditions.to_h).first
end

Finally, in the else clause of our if-statement (if “email” was not a field in the form conditions), just performs a search by the unmodified form conditions.

This will allow you to use the standard Devise session new (login) and password reset forms.

Updating the Registration Form

Modifying the template

You’ll need to update the template of your registration form to support of the format of accepting multiple email addresses, even if you’re just accepting one during registration. To do this, we’ll override the registration template by adding the file: app/views/devise/registrations/new.html.erb.

The main thing we modified compared to the source of this file is the email section. Devise expects the “email” field to be on the model in question. But we changed it so when this form is submitted, our form data is in the same shape that matches the association between our User and EmailAddress models. Posting the above form will have a data shape that looks like:

user[email_addresses_attributes[1]][email]: ""
user[password]: ""
user[password_confirmation]: ""

We can also collect more information in this form about the user if the User model is configured to accept those fields. For example, if we wanted to collect a user’s name, we would add the following to our form:

<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>

Updating the Controller

Because we’ve added new fields, we also need to update the template’s respective controller to allow the new fields to be submitted by overriding Devise’s RegistrationController as well. We will be adding a new file in app/controllers/registrations_controller.rb and specifically overriding the sign_up_params method:

Logging In Via a Primary Email

The above approach allows a user to login with any email address associated with them. If you want a user to only login with one email, there are some slight modifications you need to make.

First, add a primary column to your EmailAddress model table by creating a migration. An index on the primary field is also recommended for faster lookup.

Then in your User model, you’ll update the query in the self.find_first_by_auth_conditions method that when searching for users, to only look for email addresses that are marked as primary:

A Quick Helper

We can add an email getter method to the User model that returns the user's primary email for quick access:

Implementing with a “Contact”-like Model

Sometimes a database structure requires contact information to be separate from a User model, on a Contact model, where the Contact has multiple emails, but also has one User.

The sample Contact model in this scenario will look like:

  • first_name: string
  • last_name: string

The EmailAddress model will also slightly differ from the samples above by instead having a user_id, it would have a contact_id.

Update our Models

After we create our Contact model, we’ll setup the associations:

The only thing that will change in our User model, is the associations. The devise overrides, email_required?, will_save_change_to_email?, and find_first_by_auth_conditions, will remain in the User model. It will look like this:

Update the View

Since the login form only takes an email and password and the find_first_by_auth_conditions in the User model handles the email field not actually being on that model, we don't need to modify this form. However, we will need to make some changes to our registration form.

Any fields that were on the User model in the above examples that should go in the Contact model, we’ll wrap them in their own section. So the modified registration form will look like this:

Here, we made it so the fields name and email_addresses_attributes[1] are no longer accessed directly by the form, but instead are accessed through the contact field. This makes the shape of the data that's submitted match our associations.

Updating the Controller

Since we updated the shape our form, we have to update the shape that the controller permits:

Conclusion

It may have seemed impossible, but we just bent Devise to our will. We now know that it’s possible to have Email Addresses belong to users directly, and EmailAddresses belong to Users through a Contact model.

--

--