How to NOT implement a guest user feature for your website

Reading time: 8 minutes | Published: 03/14/2020

It may seem counter intuitive, but one of the more useful experiences in building this website is making mistakes. My first implementation of a guest user feature was a big mistake. Here is what I learned from it.

I've built user data model for my site. You see this everywhere on the internet - a user can sign up for an account on a website and log in to that account. This allows them to store information and be remembered when they return. In my case, I have one main user - me. I can log in and access features that are off limits to everyone else.

While only I can add new blog posts or new projects, other users can create an account and log in to the site. They are currently limited to doing things like creating a profile and posting comments on published blogs. I plan on adding additional features for these users; one example would be the ability to sign up for notification emails when I post a new blog.

As I started the process of building this website, I came to the realization that most people are not going to sign up for an account. If I'm lucky, they may check out a project or read a blog post, and then leave. If I'm really lucky, they will leave me a comment on a blog they read (hint, hint). I don't sign up for accounts on most websites I visit, and I'm annoyed if I get a bunch of notifications trying to get me to signup for an email list. This realization led me to the decision to create a 'guest user' feature - the ability for someone to leave a comment on a blog without the hassle of signing up. Unfortunately, I didn't do enough research about how to correctly implement this functionality, and I just started building it. (Note to self: don't do this again).

Here is how NOT to build a guest user feature for your website. I'll give you a quick summary of what I did and then link to a second blog post of how I rebuilt it.

The first thing I did was to create a single guest user in my database. I figured that everyone could use this same generic user to make comments on posts instead of having to create new guest users for each of them. A quick summary of the relationships between the data  models - a comment belongs to both a user (the author of the comment) and a 'commentable' - a polymorphic association that allows the comment to also belong to either a post or another comment (to allow for comment replies). Here is what the comments table in my database and the comment data model itself look like:

# Table from database schema
create_table 'comments', force: :cascade do |t|
  t.text 'content'
  t.string 'commentable_type'
  t.bigint 'commentable_id'
  t.bigint 'user_id'
  ...
end

# Comment class (comment data model)
class Comment < ApplicationRecord
  belongs_to :author, class_name: 'User', foreign_key: :user_id
  belongs_to :commentable, polymorphic: true
  # Allow for comments to have comments
  has_many :comments, as: :commentable, dependent: :destroy
  ...
end

# User class (user data model)
class User < ApplicationRecord
  ...
  has_many :comments, as: :commentable, dependent: :destroy
  ...
end
lang-ruby

This setup links comments to one user, which in the case of guest comments, is always the one guest user account. Like I mentioned earlier, I created a single guest user in my database seed file, a file in Rails that you can use to 'seed' your database with data to get things up and running. The first user I created was the guest user, so that it would always be the first user in the database. This is what the first part of the seed file looked like - it uses my user model to create a new user with these properties.

guest_user = User.create!(
 first_name:  "Guest",
 last_name: "User",
 email: "guest@bigdumbwebdev.com",
 password: Rails.application.credentials.dig(:seed, :guest_user_password),
 ...
lang-ruby

Then I added a form in the UI at the bottom of each post for guests to post comments:

Guest Comment Form


Using the magic of Rails ERB templates, I was able to send hidden metadata in this form to the server to tell it whether a user was logged in or not.

<div id="post-comments-container">
    <%= form_with model: @comment, id: "new-comment", class: "comment-form" do |f| %>
      <%= f.hidden_field :post_id, value: @post.id %>
      <h1 id="new-comment-header" class="comment-form-header">Got any feedback or thoughts?</h1>
      <% if logged_in? %>
        <%= f.hidden_field :user_id, value: current_user.id %>
      <% else %>
        <%= f.hidden_field :user_id, value: guest_user.id %>
        <%= f.hidden_field :guest, value: true %>
        <div id="new-guest-first-name" class="float-container">
          <%= f.label :first_name %>
          <%= f.text_field :first_name %>
        </div>
        ...
lang-html

On line 5 above, I use a helper method to determine whether the user is logged in, and depending on the answer, I send the server a 'hidden field' with either the logged in user's id, or the guest user's id. The guest_user you see on line 8 is also a helper method. It would grab the first user out of the database (which is the guest user I had created in the seed file) or a cached copy of that user.

#session_helper.rb
def guest_user
 @guest_user ||= User.first
end
lang-ruby

When the guest user submitted this form, the data would get sent to 'create' action of the Rails controller to create the comment and then show it on the page below the blog post.

def create
  @commentable = Post.find_by_id(params[:comment][:post_id])
  if guest?
    @guest = true
    guest_user.first_name = params[:comment][:first_name]
    guest_user.last_name = params[:comment][:last_name]
    guest_user.save
  end
  @comment = @commentable.comments.build(comment_params)
  if @comment.save
    # render create
  end
end 

private

  def guest?
    params[:comment][:guest] && params[:comment][:guest] == "true";
  end

  def comment_params
    params.require(:comment).permit(:content, :user_id)
  end

lang-ruby

On line 3 above, you see that I use the guest? method that I wrote in the controller to check if the incoming parameters from my guest comment form show that this is a guest user. If that is the case, I set the first and last name of the guest user and then save it to the database. I then build the comment from the comment parameters, which includes the guest user's id, and then I render the create view which shows the blog post page with the comment rendered. Success!!! Right??

First guest comment!
 
There I was, feeling so proud of myself that I had successfully built a feature to allow guest users to comment. I committed and pushed my changes, and waited eagerly for my first actual guest comment to come in.

And then it happened.

My good friend (also a developer) gave it a test run, and quickly discovered that I had a major issue on my hands. She added a guest comment, and...any guesses?

What's wrong with this picture?


Each comment 'belongs' to a user, and every time a new guest user filled out the form and changed the guest first/last name, it would change the first/last name for the one guest user in the database, and then all the guest comment's user names would change. *facepalm*

I realized that I had a big problem on my hands and had to rethink this whole feature. My first instinct was to add a property to the comment table in the database for 'guest_user_name', and store the guest's name directly in the comment data model to try to decouple the comment from the single guest user I had in the database. But since I had gotten into this mess in the first place by not reading about how to properly do this, I decided to consult my good friend Google and see what other architectures I could use to do this right.

Natalya B said:

Nice post. I appreciate you walking through your initial logic, even if it was flawed. It's all too relatable.  Looking forward to reading about the fix.

About 4 years ago

Anonymous User replied:

About 4 years ago