class Comment < ApplicationRecord
scope :newest_first, -> { order(created_at: :desc, id: :desc) }
def self.after(cursor)
return newest_first unless cursor
where("(created_at, id) < (?, ?)", cursor.fetch(:created_at), cursor.fetch(:id)).newest_first
end
end
class CommentsController < ApplicationController
def index
cursor = params[:cursor].present? ? JSON.parse(params[:cursor], symbolize_names: true) : nil
@comments = Comment.after(cursor).limit(50)
last = @comments.last
@next_cursor = last ? { created_at: last.created_at.iso8601, id: last.id } : nil
end
end
OFFSET gets slower as tables grow and becomes inconsistent under writes. Keyset pagination is stable and fast: paginate by (created_at, id) cursor. This is a common “senior Rails” upgrade for activity feeds.