dotmd
// Config Record

>Ruby on Rails 8

Claude Code instructions for modern Rails 8 apps with Solid adapters, params.expect, and service-friendly conventions.

author:
dotmd Team
license:CC0
published:Feb 23, 2026
// Installation

>Add this file to your project repository:

  • Claude Code
    CLAUDE.md
// File Content
CLAUDE.md
1# CLAUDE.md — Ruby on Rails 8
2
3## Project Overview
4
5This is a Ruby on Rails 8 application. Rails 8 ships with Solid Queue, Solid Cache, Solid Cable, Propshaft, and import maps as defaults. Do not introduce Redis, Sidekiq, Sprockets, or Webpacker — they are not needed.
6
7## Commands
8
9```bash
10# Development
11bin/rails server # Start dev server (port 3000)
12bin/rails console # Interactive console
13bin/rails routes # Show all routes (pipe to grep)
14bin/dev # Start all processes via Procfile.dev (Solid Queue, CSS watch, etc.)
15
16# Testing
17bin/rails test # Run all tests
18bin/rails test test/models/ # Run model tests only
19bin/rails test test/system/ # Run system tests (requires headless Chrome)
20bin/rails test test/models/user_test.rb:42 # Run single test by line number
21
22# Database
23bin/rails db:migrate # Run pending migrations
24bin/rails db:rollback # Undo last migration
25bin/rails db:seed # Run seeds
26bin/rails db:reset # Drop, create, migrate, seed
27
28# Generators
29bin/rails generate model User name:string email:string:uniq
30bin/rails generate controller Articles index show
31bin/rails generate migration AddRoleToUsers role:integer
32bin/rails generate authentication # Rails 8 built-in auth (generates User, Session, passwords)
33bin/rails destroy model User # Undo a generator
34
35# Code quality
36bin/rubocop # Lint (if present)
37bin/brakeman # Security scan (if present)
38```
39
40## Architecture — Rails 8 Defaults
41
42### Solid Adapters (not Redis)
43
44Rails 8 uses database-backed adapters by default:
45
46- **Solid Queue** — Background jobs. Config in `config/solid_queue.yml` and `config/queue.yml`. Workers run via `bin/jobs` or as part of `bin/dev`.
47- **Solid Cache** — Cache store. Config in `config/solid_cache.yml`. Set in `config/environments/production.rb` as `config.cache_store = :solid_cache_store`.
48- **Solid Cable** — Action Cable adapter. Config in `config/solid_cable.yml` and `config/cable.yml`.
49
50These use separate SQLite databases in development (`db/queue.sqlite3`, `db/cache.sqlite3`, `db/cable.sqlite3`). Do not add `redis` to the Gemfile for caching or jobs.
51
52### Propshaft (not Sprockets)
53
54Asset pipeline is Propshaft. There is no `app/assets/config/manifest.js`. Assets in `app/assets/` are served directly — no compilation, no fingerprinting config files. CSS goes in `app/assets/stylesheets/`. If the project uses Tailwind, it compiles via `bin/rails tailwindcss:build`.
55
56### Import Maps (not Webpack/esbuild)
57
58JavaScript uses import maps by default. Pin dependencies in `config/importmap.rb`:
59
60```ruby
61pin "application"
62pin "@hotwired/turbo-rails", to: "turbo.min.js"
63pin "@hotwired/stimulus", to: "stimulus.min.js"
64pin_all_from "app/javascript/controllers", under: "controllers"
65```
66
67Add new JS deps with `bin/importmap pin <package>`, not `yarn add` or `npm install`. If the project uses jsbundling-rails instead, there will be a `package.json` — check before assuming import maps.
68
69### Kamal Deployment
70
71Rails 8 ships with Kamal for deployment. Config lives in `config/deploy.yml` and `.kamal/`. Secrets go in `.kamal/secrets` (sourced from env or credential files). Do not commit secrets or modify `.kamal/secrets` without asking.
72
73## File Structure Conventions
74
75```
76app/
77 models/ # Business logic, validations, associations, scopes
78 controllers/ # Thin — params, auth, delegation to models
79 views/ # ERB templates (or whatever the project uses)
80 jobs/ # ApplicationJob subclasses (Solid Queue)
81 mailers/ # ActionMailer classes
82 helpers/ # View helpers (use sparingly)
83 channels/ # Action Cable channels (Solid Cable)
84 javascript/ # Import-mapped JS, Stimulus controllers
85 controllers/ # Stimulus controllers (named *_controller.js)
86config/
87 routes.rb # Route definitions
88 database.yml # DB config (SQLite default in dev)
89 solid_queue.yml # Queue backend config
90 importmap.rb # JS dependency pins
91db/
92 migrate/ # Timestamped migrations
93 schema.rb # Authoritative schema (auto-generated, committed)
94 seeds.rb # Seed data
95test/
96 models/ # Unit tests
97 controllers/ # Functional tests
98 system/ # Integration tests (Capybara + headless Chrome)
99 fixtures/ # Test data (YAML)
100 test_helper.rb # Test configuration
101```
102
103## Model Patterns
104
105### Associations
106
107Declare associations at the top of the model, before validations:
108
109```ruby
110class Article < ApplicationRecord
111 belongs_to :author, class_name: "User"
112 has_many :comments, dependent: :destroy
113 has_one_attached :cover_image # Active Storage
114end
115```
116
117Always specify `dependent:` on `has_many`. Use `dependent: :destroy` for owned records, `dependent: :nullify` for shared ones.
118
119### Validations
120
121```ruby
122validates :email, presence: true, uniqueness: { case_sensitive: false }
123validates :title, presence: true, length: { maximum: 255 }
124validates :status, inclusion: { in: %w[draft published archived] }
125```
126
127Prefer database-level constraints (NOT NULL, unique indexes) alongside model validations. Never rely on model validations alone for data integrity.
128
129### Scopes
130
131```ruby
132scope :published, -> { where(status: "published") }
133scope :recent, -> { order(created_at: :desc) }
134scope :by_author, ->(user) { where(author: user) }
135```
136
137Keep scopes simple and composable. If a scope has complex logic, extract to a class method.
138
139### Callbacks — Use Sparingly
140
141Acceptable: `before_validation`, `after_create_commit` (for jobs/broadcasts).
142Avoid: `after_save`, `after_commit` for side effects that should be explicit. Prefer service objects or explicit method calls for complex workflows.
143
144```ruby
145# Good — broadcasting after commit
146after_create_commit -> { broadcast_prepend_to("articles") }
147
148# Bad — hidden side effect
149after_save :send_notification, :update_analytics, :sync_to_crm
150```
151
152### Enums (Rails 8 syntax)
153
154```ruby
155enum :status, { draft: 0, published: 1, archived: 2 }, default: :draft
156```
157
158Always back enums with integer columns and explicit mappings. Never use array syntax.
159
160## Controller Patterns
161
162### Keep Controllers Thin
163
164```ruby
165class ArticlesController < ApplicationController
166 before_action :set_article, only: %i[show edit update destroy]
167
168 def index
169 @articles = Article.published.recent.limit(20)
170 end
171
172 def create
173 @article = Current.user.articles.build(article_params)
174
175 if @article.save
176 redirect_to @article, notice: "Article created."
177 else
178 render :new, status: :unprocessable_entity
179 end
180 end
181
182 private
183
184 def set_article
185 @article = Article.find(params.expect(:id))
186 end
187
188 def article_params
189 params.expect(article: [:title, :body, :status])
190 end
191end
192```
193
194### Key Conventions
195
196- Use `params.expect` (Rails 8) instead of `params.require().permit()` — it's stricter and raises on missing keys.
197- Return `status: :unprocessable_entity` on failed form submissions (Turbo requires this).
198- Use `Current.user` (via `Current` attributes) instead of a `current_user` helper when the authentication generator is used.
199- Respond with `redirect_to` on success, `render :action, status:` on failure.
200
201### Authentication (Rails 8 Built-in)
202
203If `bin/rails generate authentication` was run, the app has:
204- `User` model with `has_secure_password`
205- `Session` model for session management
206- `Authentication` concern included in `ApplicationController`
207- `Current` class with `.user` and `.session`
208
209Do not install Devise or other auth gems unless the project explicitly uses them.
210
211## Testing
212
213Rails uses **Minitest** by default. Do not switch to RSpec unless the project already uses it.
214
215### Model Tests
216
217```ruby
218class ArticleTest < ActiveSupport::TestCase
219 test "validates presence of title" do
220 article = Article.new(title: nil)
221 assert_not article.valid?
222 assert_includes article.errors[:title], "can't be blank"
223 end
224
225 test "published scope returns only published articles" do
226 assert_includes Article.published, articles(:published_one)
227 assert_not_includes Article.published, articles(:draft_one)
228 end
229end
230```
231
232### Controller Tests
233
234```ruby
235class ArticlesControllerTest < ActionDispatch::IntegrationTest
236 test "should get index" do
237 get articles_url
238 assert_response :success
239 end
240
241 test "should create article" do
242 assert_difference("Article.count") do
243 post articles_url, params: { article: { title: "Test", body: "Content" } }
244 end
245 assert_redirected_to article_url(Article.last)
246 end
247end
248```
249
250### System Tests
251
252```ruby
253class ArticleFlowTest < ApplicationSystemTestCase
254 test "creating an article" do
255 visit new_article_url
256 fill_in "Title", with: "My Article"
257 fill_in "Body", with: "Article content"
258 click_on "Create Article"
259
260 assert_text "Article created"
261 assert_text "My Article"
262 end
263end
264```
265
266### Fixtures over Factories
267
268Rails default is fixtures (`test/fixtures/*.yml`). Use them. They're loaded once, fast, and support associations via labels:
269
270```yaml
271# test/fixtures/articles.yml
272published_one:
273 title: First Article
274 status: published
275 author: alice # References users(:alice)
276
277draft_one:
278 title: Draft Article
279 status: draft
280 author: alice
281```
282
283Do not add FactoryBot unless the project already uses it.
284
285## Background Jobs (Solid Queue)
286
287```ruby
288class ArticlePublishJob < ApplicationJob
289 queue_as :default
290
291 def perform(article)
292 article.update!(status: "published", published_at: Time.current)
293 ArticleMailer.published_notification(article).deliver_later
294 end
295end
296```
297
298Enqueue with `ArticlePublishJob.perform_later(article)`. For scheduled jobs, use `set(wait:)`:
299
300```ruby
301ArticlePublishJob.set(wait: 1.hour).perform_later(article)
302```
303
304Recurring jobs are configured in `config/recurring.yml`:
305
306```yaml
307production:
308 article_cleanup:
309 class: ArticleCleanupJob
310 schedule: every day at 3am
311```
312
313## Database
314
315- **SQLite** is the default for development and can be used in production for moderate traffic.
316- Always add indexes for foreign keys and columns used in WHERE/ORDER clauses.
317- Use `change` in migrations (not `up`/`down`) unless the migration is irreversible.
318- Never edit `db/schema.rb` by hand — it's auto-generated from migrations.
319- Use `bin/rails db:migrate` then commit both the migration and the updated `schema.rb`.
320
321```ruby
322class AddPublishedAtToArticles < ActiveRecord::Migration[8.0]
323 def change
324 add_column :articles, :published_at, :datetime
325 add_index :articles, :published_at
326 end
327end
328```
329
330## Turbo and Hotwire
331
332Rails 8 uses Hotwire (Turbo + Stimulus) for interactivity. Key rules:
333
334- Form submissions that fail must return `status: :unprocessable_entity` or Turbo won't replace the form.
335- Use `turbo_stream` responses for partial page updates, not full JSON APIs for in-app interactions.
336- Stimulus controllers live in `app/javascript/controllers/` and are auto-registered.
337- Turbo Frames (`<turbo-frame>`) scope navigation. Turbo Streams update specific DOM elements.
338
339## Anti-Patterns to Avoid
340
3411. **Do not add gems that duplicate Rails 8 features.** No Sidekiq (use Solid Queue), no Redis gem for caching (use Solid Cache), no Devise (use built-in authentication unless already present).
342
3432. **Do not create service objects for simple CRUD.** Rails controllers and models handle this. Only extract services for complex multi-step workflows.
344
3453. **Do not use `respond_to` with JSON in a Hotwire app.** Use Turbo Streams for dynamic updates. JSON APIs are for separate API-only endpoints.
346
3474. **Do not put query logic in controllers.** Use model scopes or class methods.
348
3495. **Do not skip the migration and edit schema.rb.** Always create a migration.
350
3516. **Do not use `update_attribute` (skips validations).** Use `update` or `update!`.
352
3537. **Do not wrap single-record finds in rescue blocks.** Let `ActiveRecord::RecordNotFound` bubble up — Rails renders 404 automatically.
354
3558. **Do not create initializers for Solid Queue/Cache/Cable.** They are configured via YAML files in `config/`, not Ruby initializers.
356
3579. **Do not use `rails` directly.** Always use `bin/rails` to ensure the correct bundled version.
358
35910. **Do not add `attr_accessible` or `attr_accessor` for form fields.** Strong parameters (`params.expect`) handle mass assignment protection.
360
361## Credentials
362
363Rails uses encrypted credentials. Never commit secrets in plain text.
364
365```bash
366bin/rails credentials:edit # Edit default credentials
367bin/rails credentials:edit --environment production # Edit production credentials
368```
369
370Access with `Rails.application.credentials.dig(:aws, :access_key_id)`.
371
372## Style
373
374- Two-space indentation (Ruby community standard).
375- Use `frozen_string_literal: true` magic comment at the top of every Ruby file.
376- Prefer `%i[]` for symbol arrays, `%w[]` for string arrays.
377- Use guard clauses over nested conditionals.
378- Name boolean methods with `?` suffix. Name dangerous methods with `!` suffix.
379