Mark Thomas Miller's logo

How to hide IDs from page URLs in Ruby on Rails

April 26, 2019

Ruby on Rails uses transparent URLs by default: x.com/users/4 signals that this is the 4th user on the site. But sometimes, you don't want to let someone know that they're your 4th user! This post will teach you how to hide these ids; in other words, to turn x.com/users/4 into x.com/users/x3g29z.

To do this, we'll implement a gem called hashid-rails by Justin Cypret. It's an 80-line wrapper for the Hashids project, and it can do all the work from the model. This allows your database to use integers as IDs, but the ID will appear as a hash like x4g59d in the URL. Add it to your Gemfile:

gem "hashid-rails", "~> 1.0"

And then run bundle install.

Create a file at config/initializers/hashid.rb, and add the following. Customize config.salt to a word or phrase that you like - it'll scramble the ID to your project.

Hashid::Rails.configure do |config|
  # The salt to use for generating hashid. Prepended with table name.
  config.salt = "Mark is the best"

  # The minimum length of generated hashids
  config.min_hash_length = 6

  # The alphabet to use for generating hashids
  config.alphabet = "abcdefghijklmnopqrstuvwxyz" \
                    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
                    "1234567890"

  # Whether to override the `find` method
  config.override_find = true

  # Whether to sign hashids to prevent conflicts with regular IDs (see https://github.com/jcypret/hashid-rails/issues/30)
  config.sign_hashids = true
end

Then, in whatever model you'd like a hashed URL, include Hashid::Rails. This is what it would look like inside models/user.rb:

class User < ApplicationRecord
  include Hashid::Rails
end

Now, start up your server, take a look at your user (or whatever type of record you're implementing this on), and it'll have a hashed URL!

The only problem is that it can still be accessed via its numerical URL. For instance, although you can access it at x.com/users/g7j3dV, you can still access it at x.com/users/2, too. Let's fix that so people can't look through your records sequentially.

I recommend turning off config.override_find inside the Hashid initializer that we created earlier. This isn't necessary, but it helps us avoid shooting ourselves in the foot as we continue developing the application:

# Whether to override the `find` method
config.override_find = false

And inside your controller, use find_by_hashid instead of find:

# users_controller.rb
# ...

def set_user
  @user = User.find_by_hashid(params[:id])
end

Now, people can't look at users via their ID – they need to access them via hash.

If you enjoyed this post, please make a t-shirt with my face on it.