ruby 96 lines · 3 tabs

Polymorphic “Visible To” Scope with Arel

Shared by codesnips Jan 2026
3 tabs
class Document < ApplicationRecord
  belongs_to :owner, class_name: "User"
  has_many :visibilities, class_name: "DocumentVisibility", dependent: :delete_all

  scope :public_documents, -> { where(is_public: true) }

  scope :visible_to, ->(actor) do
    DocumentVisibilityQuery.new(self, actor).relation
  end

  def grant_to(subject)
    visibilities.create!(subject: subject)
  end
end

class DocumentVisibility < ApplicationRecord
  belongs_to :document
  belongs_to :subject, polymorphic: true
end
3 files · ruby Explain with highlit

This snippet shows how to build a reusable visible_to scope that answers a common authorization question — "which records is this actor allowed to see?" — when visibility can come from several different sources at once. Rather than filtering in Ruby after loading rows, the logic is pushed down into a single SQL query composed with Arel, so pagination, counting, and further chaining all stay correct and efficient.

The Document model defines the domain: a document can be public, owned directly by a user, or shared through a polymorphic visibilities association whose subject is either a User or a Team. The visible_to scope delegates to a query object so the model stays thin, and it accepts any actor that can describe its own visibility principals via team_ids.

The interesting work lives in DocumentVisibilityQuery. It builds three independent Arel predicates against the documents and document_visibilities tables: public_condition matches anything flagged public, owner_condition matches the actor's own rows, and shared_condition matches a polymorphic grant using both subject_type and subject_id. Building the EXISTS subquery with Arel::SelectManager keeps the correlation (vis[:document_id].eq(docs[:id])) explicit and avoids an accidental cartesian join. Combining the pieces with or produces one predicate, which is handed to ActiveRecord via where(condition) — so the caller still gets a normal, chainable ActiveRecord::Relation.

Using Arel here rather than string interpolation matters: table and column references go through arel_table, values are bound as parameters, and the subject_type/subject_id pair is matched together, which is exactly the trap that makes naive polymorphic queries leak rows across types. The EXISTS form also short-circuits per document and plays well with a composite index on (subject_type, subject_id, document_id).

The DocumentsController shows the payoff at the call site. Document.visible_to(current_user) is used as an ordinary scope, then merged with search and pagination without any special casing, and the same scope is reused to authorize a single show lookup so the read path can never diverge from the list path. The trade-off is that Arel is verbose and version-sensitive; the benefit is one authoritative, index-friendly definition of visibility that is impossible to bypass by loading first and filtering later.


Related snips

Share this code

Here's the card — post it anywhere.

Polymorphic “Visible To” Scope with Arel — share card
Link copied