Convention over configuration. New code: Rails 8.1 + Ruby 3.4 when the project allows it. Pure language: skill ruby. Preserve existing Rails 7.x; no “drive-by upgrade”.
Stack and scope
| Ask | Role |
|---|---|
| Rails 8.1 | Router, MVC, generators |
| Active Record | Persistence, migrations |
| Propshaft | Assets (default 8) |
| Hotwire | Turbo + Stimulus in HTML apps |
| Solid Queue / Cache / Cable | Jobs, cache, DB-backed pubsub |
| Kamal | Deploy (if scope includes it) |
| Credentials / ENV | Secrets |
Separate frontend (SPA/mobile): --api or config.api_only. Do not force Turbo into a JSON API.
Organization
app/
controllers/
models/
views/ # omitido em api_only
jobs/
mailers/
policies/ # se o projeto usa Pundit/ActionPolicy
services/ # POROs de caso de uso (quando o model engordar)
config/
db/migrate/
test/ # ou spec/
- Fat models with care: validations and invariants in the model; multi-model orchestration in service/PORO.
app/servicesis not a trash can — one object per use case.- Concerns only for real duplication, not to hide god objects.
Non-negotiable rules
- Strong parameters on every controller input.
- Authz on the server (Pundit/ActionPolicy/scopes); never just hide the button in the view.
- SQL: query methods / binds; never interpolate input into an SQL string.
- Reversible migrations when possible; do not edit a migration already applied in production.
- Secrets:
Rails.application.credentialsor ENV — never commit keys. - CSRF enabled in HTML; APIs with explicit token/session.
- Idempotent jobs; serializable arguments (IDs, not giant AR objects).
Controllers and routes
class PostsController < ApplicationController
before_action :set_post, only: %i[show update destroy]
def create
@post = Current.user.posts.build(post_params)
if @post.save
redirect_to @post
else
render :new, status: :unprocessable_content
end
end
private
def set_post
@post = Current.user.posts.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body)
end
end
- Scoping by user/tenant in the
find—Post.find(params[:id])on a private resource is IDOR. - Correct HTTP statuses (
404,401,403,422). - JSON:
jbuilder/alba/ project serializers; notas_jsonloose with internal columns. Currentattributes for request store (user, request_id) — automatic reset.
Active Record
- Validations + database constraints (unique, FK, check).
has_many/belongs_towithinverse_ofwhen cycles get in the way.- N+1:
includes/preload/strict_loadingin dev. - Enums,
normalizes,composed_ofwhen the domain calls for it. - Transactions for composite writes.
- Queries: chainable scopes;
find_by!when absence is an error.
Hotwire (HTML)
- Turbo Drive/Frames/Streams for UX without an SPA.
- Stimulus for minimal JS (clipboard, toggle) — business logic stays on the server.
turbo_streamresponses as needed; do not replace the app with a React SPA without being asked.- Forms:
form_with, errors in the model,status: :unprocessable_content.
Jobs, mailers, realtime
- Solid Queue (default 8) unless the repo already uses Sidekiq/GoodJob — do not swap the backend in a feature.
deliver_laterin mailers; previews inActionMailer::Preview.- Solid Cable / Solid Cache when 8 already configured it; Redis only if it is already the standard.
Frontend and API
- HTML+Hotwire: views + partials + thin helpers.
- API mode: versioning if public (
/api/v1); documented token/session auth. - Contracts: skill
api-design.
Tests
- Fixtures or FactoryBot — whichever the project uses.
- Controller/request specs for authz and strong params.
- Model specs for invariants; system tests (Capybara) for the critical Hotwire flow.
bin/rails test/bundle exec rspecin scope.
Deploy
- Kamal if the repository already has
config/deploy.ymlor the user asks for deploy. RAILS_MASTER_KEY/ credentials per environment.- Migrations in the release (
kamal/ pipeline), notdb:migrateforgotten manual. - Image: skill
docker.
Anti-patterns
Post.find(params[:id])without ownership- Cascading AR callbacks (emails, HTTP, jobs) invisible
update_all/delete_allwithout scope- Editing an old migration
- Introducing Sidekiq + Solid Queue together
- Assuming Webpacker / classic Sprockets in an app 8 (Propshaft)
- Billing/authz logic only in Stimulus
Integration with other skills
- Language:
ruby - HTTP:
api-design - Authz:
authentication-authorization - Image:
docker - Local Compose:
docker-compose - Go-live:
production-readiness
Completion criteria
- Strong params + authz/scoping on the server
- New migration if the schema changed
- Async jobs/mailers for external I/O
- HTML via Hotwire or JSON API — aligned with the client
- Tests for the flow and for negative IDOR
- Secrets out of git