rails-dhh
DHH/37signals Rails conventions — CRUD controllers, rich models with concerns, Hotwire, vanilla CSS, Solid Stack, Minitest.
Best use case
rails-dhh is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
DHH/37signals Rails conventions — CRUD controllers, rich models with concerns, Hotwire, vanilla CSS, Solid Stack, Minitest.
Teams using rails-dhh should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/rails-dhh/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How rails-dhh Compares
| Feature / Agent | rails-dhh | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
DHH/37signals Rails conventions — CRUD controllers, rich models with concerns, Hotwire, vanilla CSS, Solid Stack, Minitest.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Rails the DHH/37signals Way
This skill encodes the complete DHH/37signals Rails philosophy extracted from The Rails Doctrine, production patterns from 37signals' open-source codebases (Fizzy, Campfire, Writebook), DHH's Rails World 2025 keynote, and 265 analyzed pull requests.
**Core thesis: "Vanilla Rails is plenty."** Maximize what Rails gives out of the box. Minimize dependencies. Resist abstractions until absolutely necessary. Delete more than you add.
## Reference Guides
For detailed patterns on specific topics, read the companion files in this skill directory:
- **`hotwire.md`** — Turbo Drive/Frames/Streams/Morphing decision framework, Stimulus controller patterns, anti-patterns, gotchas
- **`css.md`** — Vanilla CSS with `@layer`, OKLCH color system, `:has()`, container queries, `@starting-style`
- **`testing.md`** — Minitest with fixtures, integration tests, DHH's testing philosophy
- **`deployment.md`** — Kamal 2, Solid Queue production config, safe migrations, Rails 8.1 features
---
## Quick Reference — Before Writing Code
Ask yourself:
1. **Can I use an existing Rails convention?** If yes, use it.
2. **Am I creating a new abstraction?** Does it earn its keep? Can you point to 3+ variations?
3. **Am I adding a dependency?** Can you build it yourself in fewer lines than the gem's README?
4. **Is this a new action on an existing controller?** Make it a new resource instead.
5. **Am I using a boolean column for state?** Use a record instead.
6. **Am I putting business logic in the controller?** Move it to the model.
7. **Am I writing a service object?** Put the logic in a concern on the model.
8. **Am I reaching for Turbo Streams?** Could morphing or a simple redirect handle it?
9. **Am I writing a Stimulus controller?** Could Turbo handle this without JS?
10. **Am I writing a system test?** Write an integration test instead.
11. **Am I writing CSS with JavaScript?** Could `:has()`, container queries, or `@starting-style` handle it?
12. **Is this migration safe?** Would it lock a large table? Use `strong_migrations`.
13. **Does this code earn its place?** Would DHH call it "anemic"?
---
## Philosophy — The Eight Pillars
1. **Rich domain models** over service objects
2. **CRUD controllers** over custom actions
3. **Concerns** for horizontal code sharing
4. **Records as state** over boolean columns
5. **Database-backed everything** (Solid Stack, no Redis)
6. **Build it yourself** before reaching for gems
7. **Ship, Validate, Refine** — deploy prototype quality, iterate on real feedback
8. **Delete more than you add** — fight "anemic" code (thin wrappers that explain nothing)
---
## The Stack
| Component | Tool | NOT This |
|------------------|-------------------------------|-----------------------|
| Framework | Rails 8.x | |
| Ruby | 3.2+ (3.3+ for YJIT) | |
| Frontend | Hotwire (Turbo 8 + Stimulus) | React, Vue, Svelte |
| JavaScript | Importmap-rails (no Node.js) | Webpack, esbuild, Vite |
| CSS | Vanilla CSS with `@layer` | Tailwind, Sass, PostCSS |
| Background Jobs | Solid Queue | Sidekiq, Resque |
| Caching | Solid Cache | Redis, Memcached |
| WebSockets | Solid Cable | Redis-backed ActionCable |
| Asset Pipeline | Propshaft | Sprockets |
| Testing | Minitest + fixtures | RSpec + FactoryBot |
| Auth | `rails generate authentication` | Devise |
| Deployment | Kamal 2 + Thruster | Heroku, Fly.io |
| Server | Puma | |
---
## Architecture Rules
### Everything is CRUD
Every action maps to a CRUD verb. When something doesn't fit, create a new resource:
```ruby
# ❌ BAD: Custom actions on existing resource
resources :cards do
post :close
post :reopen
end
# ✅ GOOD: New resources for each state change
resources :cards do
resource :closure # POST to close, DELETE to reopen
resource :pin # POST to pin, DELETE to unpin
resource :watch # POST to watch, DELETE to unwatch
end
```
Controllers have only the 7 RESTful actions: `index`, `show`, `new`, `create`, `edit`, `update`, `destroy`. If you need something else, you need a new controller.
### Thin Controllers, Rich Models
Controllers orchestrate. Models contain business logic:
```ruby
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close # All logic lives in the model
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.html { redirect_to @card.board }
end
end
def destroy
@card.reopen
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.html { redirect_to @card.board }
end
end
end
```
### State as Records, Not Booleans
Instead of `closed: boolean`, create a separate model. This gives you who did it, when, and easy scoping for free:
```ruby
# Migration
create_table :closures do |t|
t.references :card, null: false, foreign_key: true
t.references :user, foreign_key: true
t.timestamps
end
# Model
class Closure < ApplicationRecord
belongs_to :card, touch: true
belongs_to :user, optional: true
end
# Concern on parent
module Card::Closeable
extend ActiveSupport::Concern
included do
has_one :closure, dependent: :destroy
scope :closed, -> { joins(:closure) }
scope :open, -> { where.missing(:closure) }
end
def close(user: Current.user)
unless closed?
transaction do
create_closure!(user: user)
track_event(:closed, creator: user)
end
end
end
def closed?
closure.present?
end
def reopen
closure&.destroy!
end
end
```
### Concerns for Horizontal Sharing
Models include many small, self-contained concerns:
```ruby
class Card < ApplicationRecord
include Assignable, Closeable, Eventable, Pinnable,
Searchable, Taggable, Watchable
belongs_to :board
belongs_to :creator, class_name: "User", default: -> { Current.user }
end
```
Each concern owns its associations, scopes, validations, and methods. A concern should be understandable in isolation.
Controller concerns share behavior across controllers:
```ruby
module CardScoped
extend ActiveSupport::Concern
included do
before_action :set_card
end
private
def set_card
@card = Card.find(params[:card_id])
end
end
```
### Database-Backed Everything
No Redis. Use the Solid Stack:
- `solid_queue` for background jobs
- `solid_cache` for caching
- `solid_cable` for WebSockets
Configure separate databases in `config/database.yml`:
```yaml
production:
primary:
<<: *default
database: app_production
queue:
<<: *default
database: app_production_queue
migrations_paths: db/queue_migrate
cache:
<<: *default
database: app_production_cache
migrations_paths: db/cache_migrate
cable:
<<: *default
database: app_production_cable
migrations_paths: db/cable_migrate
```
### Fight Anemic Code
DHH's test: **Does this abstraction earn its keep?** If you can't point to 3+ variations that need an abstraction, inline it.
- If a private method just wraps one line, inline it
- If a concern has one method, put it on the model
- If a helper reformats one value, do it in the view
- If a service object just calls one model method, remove it
### Data Integrity at the Database Level
Enforce constraints in the database, not just application validations:
```ruby
create_table :cards do |t|
t.string :title, null: false
t.references :board, null: false, foreign_key: true
t.integer :position, null: false, default: 0
t.timestamps
end
add_index :cards, [:board_id, :position], unique: true
```
---
## What NOT to Do
| Pattern | Why It's Avoided | Instead |
|-------------------------|-----------------------------------------------------|----------------------------------|
| Service objects | Business logic belongs in models | Rich models with concerns |
| Form objects | Rarely needed | ActiveModel when absolutely needed |
| Decorators/Presenters | Extra indirection | Helpers + partials |
| GraphQL | Over-engineered for most apps | REST + Turbo |
| Devise | Auth is ~150 lines of custom code | `rails generate authentication` |
| RSpec | Minitest is simpler and faster | Minitest + fixtures |
| FactoryBot | Fixtures are faster and simpler | YAML fixtures |
| Tailwind | Vanilla CSS with modern features is sufficient | `@layer`, nesting, OKLCH, `:has()` |
| Redis | Database-backed everything | Solid Stack |
| ViewComponent | Extra abstraction over ERB | Partials |
| Sidekiq | Requires Redis | Solid Queue |
| `dry-rb` gems | Over-engineered for most Rails apps | Plain Ruby |
| Interactor/Command | Service objects by another name | Model methods |
| `pundit`/`cancancan` | Authorization logic belongs in models | Model methods + concerns |
| React/Vue/Svelte | Conflicts with Hotwire server-first approach | Turbo + Stimulus |
| Sass/PostCSS | Native CSS is powerful enough now | Vanilla CSS |
| System tests | "Always brittle, always broken, always slow" — DHH | Controller integration tests |
---
## Code Style
### Ruby Conventions (from Fizzy)
```ruby
# Symbol arrays with spaces inside brackets
before_action :set_message, only: %i[ show edit update destroy ]
# Private method indentation — 2 spaces after `private`
private
def set_message
@message = Message.find(params[:id])
end
# Bang methods for fail-fast
@message = Message.create!(message_params)
# Ternaries for simple conditionals
@room.direct? ? "DM" : @room.name
# Default values via lambdas
belongs_to :creator, class_name: "User", default: -> { Current.user }
# Expression-less case for complex conditionals
case
when params[:before].present?
messages.page_before(params[:before])
else
messages.last_page
end
# Current for request context
Current.user
Current.session
Current.account
```
### Naming Conventions
- **Verb methods for actions:** `card.close`, `card.reopen`, `card.gild`
- **Predicate methods for state:** `card.closed?`, `card.golden?`, `card.pinned?`
- **Concerns are adjectives:** `Closeable`, `Publishable`, `Watchable`, `Taggable`
- **Controllers are nouns:** `Cards::ClosuresController`, `Cards::PinsController`
- **`_later` and `_now` for async patterns:**
```ruby
def mark_as_read_later(user:)
MarkCardAsReadJob.perform_later(self, user)
end
def mark_as_read_now(user:)
readings.find_or_create_by!(user: user).touch
end
```
### Background Jobs — Shallow Wrappers
Jobs just call model methods. All logic stays in the model:
```ruby
class NotifyRecipientsJob < ApplicationJob
def perform(notifiable)
notifiable.notify_recipients
end
end
```
Recurring jobs in `config/recurring.yml`:
```yaml
deliver_bundled_notifications:
command: "Notification::Bundle.deliver_all_later"
schedule: every 30 minutes
cleanup_magic_links:
command: "MagicLink.cleanup"
schedule: every 4 hours
```
### HTTP Caching
Use ETags and `fresh_when` aggressively:
```ruby
class ApplicationController < ActionController::Base
etag { "v1" } # Bump to bust all caches on deploy
end
# In specific controllers
fresh_when etag: [@board, @page.records]
```
---
## Hotwire Decision Framework (Summary)
> **Full reference with code examples and anti-patterns:** Read `hotwire.md` in this directory.
```
1. Turbo Drive (free, zero code — handles all page navigation)
↓ not enough?
2. Morphing Page Refreshes (same-URL redirects, broadcasts_refreshes)
↓ not enough?
3. Turbo Frames (one self-contained region: inline edit, lazy load, modal)
↓ not enough?
4. Turbo Streams (multiple unrelated DOM updates from one response)
↓ not enough?
5. Stimulus (client-side interactivity: toggles, clipboard, animations)
↓ not enough?
6. Custom JS (last resort, wrap in Stimulus controller)
```
- **Different URL?** → Turbo Drive handles it. Do nothing.
- **Same-page redirect after form?** → Morphing: `turbo_refreshes_with method: :morph, scroll: :preserve`
- **Real-time multi-user?** → `broadcasts_refreshes` on model + `touch: true` on children
- **One self-contained region?** → Turbo Frame
- **Multiple unrelated DOM updates?** → Turbo Streams (HTTP response)
- **Client-side interactivity only?** → Stimulus controller
---
## File Organization
```
app/
├── controllers/
│ ├── concerns/ # Authentication, CardScoped, etc.
│ ├── cards/
│ │ ├── closures_controller.rb
│ │ ├── pins_controller.rb
│ │ └── watches_controller.rb
│ ├── cards_controller.rb
│ └── application_controller.rb
├── models/
│ ├── concerns/ # Closeable, Pinnable, Watchable, etc.
│ ├── card/
│ │ └── closeable.rb # Namespaced concerns for complex models
│ ├── card.rb
│ ├── closure.rb
│ ├── current.rb # CurrentAttributes
│ └── application_record.rb
├── views/
│ ├── cards/
│ │ ├── _card.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── cards/closures/
│ │ └── create.turbo_stream.erb
│ ├── layouts/
│ └── shared/ # Shared partials
├── javascript/
│ └── controllers/ # Stimulus controllers
├── assets/
│ └── stylesheets/ # Flat structure, one file per concept
│ ├── application.css # @layer imports only
│ ├── _reset.css
│ ├── base.css
│ ├── colors.css # OKLCH + dark mode
│ ├── buttons.css
│ ├── inputs.css
│ └── utilities.css
├── jobs/ # Shallow wrappers
└── mailers/
config/
├── database.yml # PostgreSQL + Solid databases
├── deploy.yml # Kamal 2
├── recurring.yml # Solid Queue recurring jobs
├── solid_queue.yml # Worker/dispatcher config
└── importmap.rb # JS dependencies (no Node.js)
test/
├── fixtures/ # YAML fixtures
├── models/
├── controllers/ # Integration tests (NOT system tests)
└── test_helper.rb
```Related Skills
rails-upgrade-assistant
Analyzes Rails applications and generates comprehensive upgrade reports with breaking changes, deprecations, and step-by-step migration guides for Rails 7.0 through 8.1.1. Use when upgrading Rails applications, planning multi-hop upgrades, or querying version-specific changes.
rails-architecture
Guides modern Rails 8 code architecture decisions and patterns. Use when deciding where to put code, choosing between patterns (service objects vs concerns vs query objects), designing feature architecture, refactoring for better organization, or when user mentions architecture, code organization, design patterns, or layered design.
37signals-rails-style
Apply 37signals/DHH Rails conventions when writing Ruby on Rails code. Use when building Rails applications, reviewing Rails code, or making architectural decisions. Covers various aspects of Rails application architecture, design and dependencies.
skill-rails-upgrade
Analyze Rails apps and provide upgrade assessments
async-falcon-rails
Transform a Rails application to use Falcon web server with async job processing (async-job), async Action Cable, and Redis-compatible database (Valkey for production). Use when the user wants to add async/Falcon stack to a Rails project, migrate from Puma to Falcon, or set up async job processing with Redis for both development and production environments including Kamal deployment.
agent-rails-expert
Expert Rails specialist mastering Rails 7+ with modern conventions. Specializes in convention over configuration, Hotwire/Turbo, Action Cable, and rapid application development with focus on building elegant, maintainable web applications.
agent-guardrails
Stop AI agents from secretly bypassing your rules. Mechanical enforcement with git hooks, secret detection, deployment verification, and import registries. Born from real production incidents: server crashes, token leaks, code rewrites. Works with Claude Code, Clawdbot, Cursor. Install once, enforce forever.
bgo
Automates the complete Blender build-go workflow, from building and packaging your extension/add-on to removing old versions, installing, enabling, and launching Blender for quick testing and iteration.
Empirical Validation
Requires proof before marking work complete — no "trust me, it works"
dubstep
Dubstep composition specialist - genre knowledge, sound design, and production patterns for LMMS
dotnet-ui-testing-core
Tests UI across frameworks. Page objects, test selectors, async waits, accessibility.
dev-server-sandbox
Run multiple isolated mux dev-server instances (temp MUX_ROOT + free ports)