Skip to content
Youness Fkhach · Writing · · 2 min readHomeWorkWritingResume

Rails signed_id is underrated

Email verification and password resets get re-implemented in every codebase. Most approaches add schema clutter or JWT machinery when Rails has a built-in solution.

Ruby on RailsSecurityArchitecture

Email verification and password resets are features every web app builds eventually. Over the years, I’ve seen dozens of custom implementations. They range from terrifyingly insecure to military-grade overengineered.

I’ve seen the full spectrum of custom token schemes:

Most of these approaches either leak state into the database or introduce unnecessary dependencies.

The Rails solution: signed_id

Rails added signed_id in version 6.1. It uses ActiveSupport::MessageVerifier under the hood to generate a tamper-proof, signed token containing the record’s ID.

No extra database columns, no custom secret management, and no external gems.

Generating a token is a single call:

user = User.find(123)
token = user.signed_id(purpose: :email_verification, expires_in: 24.hours)

Locating the user from the token:

user = User.find_signed(token, purpose: :email_verification)

If the token is tampered with, expired, or evaluated against the wrong purpose, find_signed returns nil.

Where it shines

1. Email verification

Instead of creating verification tokens in the database, the controller generates a signed ID scoped to the purpose:

# Generating the link
url = verify_email_url(token: user.signed_id(purpose: :email_verification, expires_in: 24.hours))

# Verifying the link
def show
  user = User.find_signed(params[:token], purpose: :email_verification)

  if user&.update(email_verified: true)
    redirect_to root_path, notice: "Email verified."
  else
    redirect_to new_verification_path, alert: "Invalid or expired link."
  end
end

2. Password resets

Password reset links require tight expiry windows and single-purpose isolation:

# Generating the reset token
token = user.signed_id(purpose: :password_reset, expires_in: 15.minutes)

# Processing the reset
def update
  user = User.find_signed(params[:token], purpose: :password_reset)

  if user&.update(password_params)
    redirect_to login_path, notice: "Password updated."
  else
    render :edit, status: :unprocessable_entity
  end
end

Marketing emails shouldn’t force users to log in just to change notification settings. A signed ID embeds identity safely:

unsubscribe_url = unsubscribe_url(token: user.signed_id(purpose: :unsubscribe, expires_in: 30.days))

Securing private attachments without auth headers or session cookies:

attachment = Document.find(params[:id])
download_url = document_download_url(token: attachment.signed_id(purpose: :download, expires_in: 5.minutes))

Why it’s rarely used

I think signed_id sits unused in most codebases simply because it didn’t get enough exposure when Rails 6.1 shipped. It’s tucked away in ActiveRecord documentation, so developers keep reaching for custom token tables or JWT machinery.

I believe it didn’t get the publicity it deserves for a feature that handles signing, expiration, and purpose-scoping out of the box. That’s why I’m writing this article: it solves a problem almost every Rails app has without adding schema clutter or external gems.