How do I gate a feature behind a paid subscription in Rails?
Add include RequireEntitlement to the controller. Unentitled accounts are redirected to the pricing page with a message; admins are exempt. What "entitled" means is defined separately in app/models/entitlement.rb, so you can change the gate from a subscription to a token balance or to nothing without touching any controller.
The change
class ReportsController < ApplicationController
include RequireEntitlement
end
That is the whole gate. The concern adds a before_action that runs after authentication, so
Current.account is already set when it checks.
Why the meaning lives somewhere else
RequireEntitlement deliberately does not know what entitlement is. It asks
Entitlement.for(Current.account).entitled? and acts on the answer. The definition is one line in
app/models/entitlement.rb:
def for(account) = Subscription.new(account)
Point that at a different strategy and every gated controller changes at once. The file ships with a
paid subscription as the default and an Open strategy that gates nothing, which is what the
remove-billing skill switches to when an app turns out to be free. No controller is edited either
way.
What the gate does not do
It answers one question: may this workspace use this surface at all. It says nothing about whether
this user may see this particular record. That is Pundit's job, and the two are orthogonal. A gated
controller still calls authorize and policy_scope, or a paying customer can read another paying
customer's rows.
Check it both ways
A gate is only real if a spec proves the closed case:
bin/rspec spec/requests/projects_spec.rb
Two examples, minimum: an entitled account gets 200, an unentitled one is redirected. The example
Project slice has both, and it is the shape to copy.
Entitlement reads never call Stripe
The check reads a local Subscription row that the webhook keeps in sync. That matters for latency
and for failure modes: a Stripe outage does not log out your paying customers, because nothing at
request time talks to Stripe at all.