Skip to content
A Rails plugin for exposing non-sequential (Youtube-like) string IDs instead of the sequential integer IDs provided by Rails
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.



A Rails plugin for exposing non-sequential (Youtube-like) string IDs instead of the sequential integer IDs provided by Rails.

Before, your API may look like

GET /users/123
  "id": 123,
  "name": "Alice O'User"


GET /users/9w63Hubh4oL
  "id": "9w63Hubh4oL",
  "name": "Alice O'User"


Exposing sequential integer IDs has several drawbacks:

Why not use UUIDs?

"But why not just use UUIDs", you ask? Rails has built-in support for them. But they are very long. Exposing them in an API is okay, but in a URL just doesn't look nice

Even base62 encoding that ID is very long

64-bit integers would be optimal, but they can't be random: the risk of collisions would be too high.

Our solution

Rails makes heavy use of sequential integer IDs internally, but there's no need of exposing them. ActsAsHavingStringId provides an alternative string representation of your IDs. This representation is

base62(tea(id, md5( + Rails.application.secrets.string_id_key)))

The representation looks something like "E0znqip4mRA".

tea above is the "New variant" of the Tiny Encryption Algorithm. You should probably not consider your id to be forever secret, but it should be pretty hard to figure out from the string representation.

Your controllers will continue to work without modification, but will start to accept string IDs. So if worked before, something like should magically work.


First, set up your secrets.yml:

  string_id_key: notverysecret

  string_id_key: notverysecreteither

  string_id_key: <%= ENV["STRING_ID_KEY"] %>

Then, call the method in your model class, after any relations to other models:

class MyModel < ApplicationRecord
  has_many :my_other_model

The id of your model will now not be an int, but rather an instance of ActsAsHavingStringId::StringId. As an example:

> m = MyModel.create!
=> 1/7EajpSfdWIf
=> 1
=> "7EajpSfdWIf"

All ActiveRecord functions will continue to accept int IDs, but will now also accept the string representation as input:

> MyModel.find("7EajpSfdWIf")
=> #<MyModel id: 1/7EajpSfdWIf, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">
> MyModel.where(id: "7EajpSfdWIf")
=> #<ActiveRecord::Relation [#<MyModel id: 1/7EajpSfdWIf, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">]>

In all associated models, foreign keys to your model will also be this new type of id.

> MyOtherModel.create! my_model: MyModel.first
=> #<MyOtherModel id: 1, my_model_id: 1/GBpjdLndSR0, created_at: "2016-09-07 10:32:24", updated_at: "2016-09-07 10:32:24">

Then, for exposing your string ID, make sure to always use id.to_s. For example, if you're using ActiveModelSerializers:

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name

  def id

You can get the string representation of an ID from a class without having the instance

> MyModel.id_string(1)
=> "7EajpSfdWIf"

And, conversely, getting the ID from the string representation

> MyModel.id_int("7EajpSfdWIf")
=> 1

And that's just about it!

But I'm getting these weird type cast errors

If you have has_many :through relations in your app, you may need to add a few associations more in your app in order for Rails to understand how your data model fits together. For example in this model

class Author < ApplicationRecord

class Blog < ApplicationRecord
  has_many :posts
  has_many :authors, through: :posts

class Post < ApplicationRecord
  belongs_to :blog
  belongs_to :author

an attempt to do Blog.first.authors will raise a TypeError: can't cast ActsAsHavingStringId::StringId.

In order to fix this, you'll need to add the missing association has_many :posts to Author.


  • Since the MyModel.find("7EajpSfdWIf") functionality depends on the argument now being a string, MyModel.find("5") will no longer mean MyModel.find(5), but rather MyModel.find(4387534) or something. Is that a problem?
  • It's a potential security problem that we don't force strings from controllers (integer id coming from JSON postdata will make it find by original id)


Add this line to your application's Gemfile:

gem 'acts_as_having_string_id'

And then execute:

$ bundle

Or install it yourself as:

$ gem install acts_as_having_string_id


To contribute, fork and clone the repo, edit the code (don't change the version number of the gem). Add tests, run them using


Then create a pull request.

To build the gem (this is mostly for myself), run

gem build acts_as_having_string_id.gemspec


The Tiny Encryption Algorithm was created by David Wheeler and Roger Needham of the Cambridge Computer Laboratory. This library's implementation is based on this code by Jeremy Hinegardner.


The gem is available as open source under the terms of the MIT License.

You can’t perform that action at this time.