Permission UI the Rails way

I’ve built many a SaaS. And most have pricing tiers: the higher the plans the more widgets you can create or features are allowd. Typically there are two ways to approach this:

  • hide the feature if the customer does not have access;
  • show all features, to all customers, but guard them with some, optional, upsell.

I have succesfully implementated the latter approach with a helper that quacks like Rails itself.

The helper lets you wrap content and decide what happens when access is denied. Hide it, redirect to a link, render content or render a partial. It’s small but flexible. This is how it looks (added a toggle allowed/denied states):

As always view on GitHub for a ready-to-copy code.

The API is elegant. Let’s look at some examples:

<%= allowed? Current.user.premium? do %>
  <%= link_to "Create project", projects_path %>
<% end %>

When Current.user.premium? is true, the link renders. When false, nothing shows. That’s the default behavior.

But you can do more. Redirect to a link instead:

<%= allowed? Current.user.premium?, redirect_to: upgrade_path do %>
  <%= link_to "Advanced analytics", analytics_path %>
<% end %>

The content becomes a clickable link pointing to your upgrade page.

Show plain text as a fallback:

<%= allowed? Current.user.premium?, render: { plain: "Upgrade to unlock this." } do %>
  <%= button_to "Delete project", project_path, method: :delete %>
<% end %>

Or render HTML:

<%= allowed? Current.user.premium?, render: { html: "Upgrade to the <strong>Pro plan</strong>." } do %>
  <%= button_to "Delete project", project_path, method: :delete %>
<% end %>

And render a partial for complex fallbacks:

<%= allowed? Current.user.premium?, render: "upsell" do %>
  <%= button_to "Advanced settings", settings_path %>
<% end %>

The helper

Here’s the full helper in app/helpers/allowed_helper.rb:

module AllowedHelper
  def allowed?(condition, options = {})
    return yield if condition

    if options[:redirect_to]
      content = capture { yield }

      link_to strip_tags(content), options[:redirect_to]
    elsif options.key?(:render)
      render_options = options[:render]

      if render_options.is_a?(Hash) && render_options.key?(:html)
        render_options[:html].html_safe
      elsif render_options.is_a?(Hash) && render_options.key?(:plain)
        render_options[:plain]
      else
        render({ partial: render_options.to_s }) if render_options.is_a?(String) || render_options.is_a?(Symbol)
      end
    end
  end
end

The logic is straightforward. If the condition is true, render the block. If false, check your options and handle it accordingly.

When you pass redirect_to, it captures the block content, strips HTML tags and wraps it in a link. When you pass render, it supports three formats: plain text, raw HTML or a partial name as a string or symbol.

This helper did, ~10 years ago, not start like this. Over the years, after working on many dozens Rails-based SaaS apps, my aim became to make helpers “quack” more like Rails methods. You’re familiar with render plain:, render html: and partial rendering. This helper just extends that familiar syntax to conditional rendering.

I like it. What do you think of this?

Product-minded Rails notes

Once a month: straightforward notes on improving UX in Rails—what to simplify, what to measure, and UI/frontend changes that move real usage.

Over to you…

What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!

Comments are powered by Chirp Form

Want to read me more?