rails-controllers — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited rails-controllers (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The base controller is just a composition of concerns plus a few global settings. All logic lives in the included modules.
class ApplicationController < ActionController::Base
include Authentication
include Authorization
include BlockSearchEngineIndexing
include CurrentRequest, CurrentTimezone, SetPlatform
include RequestForgeryProtection
include TurboFlash, ViewTransitions
etag { "v1" }
stale_when_importmap_changes
allow_browser versions: :modern
endDon't let `ApplicationController` become a dumping ground. Because it's already a grab-bag, every new method feels free — "will one more method among 25 really hurt?" — and the file grows into a pile of unrelated helpers (current-user checks, default redirects, production?/staging? environment checks, www-canonicalisation, response-header setters, …) that nobody dares move or delete because they might be referenced from anywhere. That's not a crisis, but it's real cognitive load on every controller in the app.
When you add a method to ApplicationController — or find one already bloated — apply three rules:
together, not be scattered, so the next reader can scan the file.
before_action running on every request earns its place here. A method used by only one or two controllers (out of 20+) does not — move it into a concern and include it only in the controllers that need it (see Pattern 2 and the *Scoped concerns below).
CDN/cache header setters becomes app/controllers/concerns/akamai.rb, leaving a single include Akamai line. The behaviour stays available but is contained, named, and testable in isolation.
# Bad: a dumping ground — unrelated helpers, no grouping, all global
class ApplicationController < ActionController::Base
def current_account; ...; end
def set_cache_headers; response.headers["Cache-Control"] = ...; end
def force_www; redirect_to "https://www.#{request.host}..." unless ...; end
def production?; Rails.env.production?; end
def purge_cdn(path); ...; end
def redirect_to_dashboard; ...; end
def staging?; Rails.env.staging?; end
def set_surrogate_key(*keys); response.headers["Surrogate-Key"] = ...; end
# ...20 more, in no particular order
end
# Good: each cluster extracted into a named concern, base stays a manifest
class ApplicationController < ActionController::Base
include Authentication # current_account & friends
include CanonicalHost # force_www, host canonicalisation
include Akamai # set_cache_headers, set_surrogate_key, purge_cdn
endWhen multiple controllers load the same parent resource, create a *Scoped concern that handles the loading and provides shared helpers:
# app/controllers/concerns/card_scoped.rb
module CardScoped
extend ActiveSupport::Concern
included do
before_action :set_card, :set_board
end
private
def set_card
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
end
def set_board
@board = @card.board
end
def render_card_replacement
render turbo_stream: turbo_stream.replace(
[ @card, :card_container ],
partial: "cards/container",
method: :morph,
locals: { card: @card.reload }
)
end
endclass Cards::CommentsController < ApplicationController
include CardScoped # sets @card and @board via before_action
before_action :set_comment, only: %i[ show edit update destroy ]
endActions like "close", "reopen", "watch" don't map to CRUD verbs. Don't add custom routes — model the state change as a singular resource nested under the parent: create = turn on, destroy = turn off.
# Bad: custom actions
resources :cards do
post :close
post :reopen
end
# Good: singular nested resources
resources :cards do
scope module: :cards do
resource :closure # POST = close, DELETE = reopen
resource :goldness # POST = gild, DELETE = ungild
resource :watch # POST = watch, DELETE = unwatch
resource :pin # POST = pin, DELETE = unpin
resource :publish # POST = publish
end
endclass Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@card.reopen
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
endTurning a verb into a noun. When you reach for a custom action, name the thing the verb produces and route to its create/destroy:
| Tempting verb action | Noun resource | Maps to |
|---|---|---|
POST cards/:id/close | resource :closure | Cards::ClosuresController#create (destroy = reopen) |
POST posts/:id/archive | resource :archive | Posts::ArchivesController#create (destroy = unarchive) |
POST posts/:id/publish | resource :publication | Posts::PublicationsController#create |
POST users/:id/follow | resource :follow | Users::FollowsController#create (destroy = unfollow) |
POST pages/:id/visit | resource :visit | Pages::VisitsController#create |
The on/off pair (create/destroy) keeps the controller RESTful and the routes guessable. Reserve member/collection custom routes for genuinely actionless endpoints that don't represent a resource.
Raw Card.find(params[:id]) bypasses access control. Always load resources through scoped associations that respect user permissions:
def set_card
# accessible_cards respects board access permissions
@card = Current.user.accessible_cards.find_by!(number: params[:id])
end
def set_user
# scoped to current account and active users only
@user = Current.account.users.active.find(params[:id])
endCentralize authorization in before_action filters named ensure_permission_to_* that head :forbidden when denied:
class CardsController < ApplicationController
before_action :set_card, only: %i[ show edit update destroy ]
before_action :ensure_permission_to_administer_card, only: %i[ destroy ]
private
def ensure_permission_to_administer_card
head :forbidden unless Current.user.can_administer_card?(@card)
end
endPut shared checks in the scoping concern (e.g. ensure_permission_to_admin_board in BoardScoped) so controllers just declare before_action :ensure_permission_to_admin_board.
Use fresh_when with ETags based on the data being rendered; Rails returns 304 Not Modified when the client's cached version matches.
def index
@columns = @board.columns.sorted
fresh_when etag: @columns
endFull treatment (multi-object ETags, stale?, global etag components): [[rails-performance]].
Use respond_to blocks for HTML, JSON, and Turbo Stream. Conventions: HTML redirects, JSON returns status codes, Turbo Stream renders the matching .turbo_stream.erb template.
def create
respond_to do |format|
format.html do
card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted")
redirect_to card
end
format.json do
card = @board.cards.create! card_params.merge(creator: Current.user, status: "published")
head :created, location: card_path(card, format: :json)
end
end
end
def update
@card.update! card_params
respond_to do |format|
format.turbo_stream
format.json { render :show }
end
end# HTML with validation errors
def update
if @user.update(user_params)
respond_to do |format|
format.html { redirect_to @user }
format.json { head :no_content }
end
else
respond_to do |format|
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
endControllers handle HTTP concerns only (params, responses, redirects). Business logic lives in model methods with intention-revealing names; plain ActiveRecord operations are fine too — no service layer in between.
class Boards::PublicationsController < ApplicationController
include BoardScoped
before_action :ensure_permission_to_admin_board
def create
@board.publish # business logic lives in Board
end
def destroy
@board.unpublish
end
end
# Plain ActiveRecord is fine
def create
@comment = @card.comments.create!(comment_params)
endFilters, tabs, search terms, and sort orders stored in session or JavaScript state make views impossible to link, bookmark, or share, and a refresh loses them. For GET actions, keep UI state in readable URL query params — shareable, bookmarkable, refresh-safe, back-button-friendly.
# /opportunities?category=acting&company=eutc&sort=newest
def index
@opportunities = Opportunity.listable
.then { |scope| params[:category].present? ? scope.where(category: params[:category]) : scope }
.then { |scope| params[:company].present? ? scope.joins(:company).where(companies: { slug: params[:company] }) : scope }
end<%# Tabs and filters are plain links that change params — no JS state %>
<%= link_to "Acting", opportunities_path(category: :acting) %>Key Points:
?category=acting, slugs) over opaque ids where possible.method: :get so submissions land in the URL.Give each public action a single comment naming its HTTP verb and path. It makes the controller scannable as a route map and catches actions that have drifted from their route (or shouldn't exist).
class PostsController < ApplicationController
# GET /posts
def index; end
# GET /posts/:id
def show; end
# POST /posts
def create; end
# DELETE /posts/:id
def destroy; end
endKeep it to one line per action; the docstring describes the route, not the implementation. bin/rails routes -c posts is the source of truth if a comment and the routes disagree.
| Pattern | When to Use |
|---|---|
| Thin ApplicationController | Always - compose with concerns |
| Resource Scoping Concerns | When multiple controllers share parent resource |
| Nested Singular Resources | Non-CRUD state changes (close, watch, pin) |
| Scoped Resource Loading | Always - load through user's accessible scope |
| Permission before_actions | Restricting actions to authorized users |
| ETags with fresh_when | Cacheable GET requests → [[rails-performance]] |
| respond_to blocks | Supporting multiple response formats |
| Thin Controllers | Always - delegate logic to models |
| URL as State | GET actions with filters, tabs, search, or sort |
| Verb→noun nested resource | Naming a non-CRUD action (archive→ArchivesController#create) |
# GET /posts/:id docstrings | Every public action — scannable route map |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.