Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/37signals-rails-style" ~/.claude/skills/majiayu000-claude-skill-registry-37signals-rails-style && rm -rf "$T"
skills/data/37signals-rails-style/SKILL.md- references API keys
37signals/DHH Rails Style Guide
Core Philosophy
- "Vanilla Rails is plenty." Maximize what Rails gives you, minimize dependencies, resist abstractions until necessary.
- Rich domain models over service objects
- CRUD controllers over custom actions
- Concerns for horizontal code sharing
- Records as state over boolean columns
- Database-backed everything (no Redis)
- Build it yourself before reaching for gems
Dependencies
Use
- Rails (edge), turbo-rails, stimulus-rails, importmap-rails, propshaft, solid_queue, solid_cache, solid_cable (database-backed, NO Redis), geared_pagination, bcrypt, rqrcode, redcarpet
Avoid
| Gem/Pattern | Why |
|---|---|
| Auth is ~150 lines of custom code |
/ | Authorization lives in models |
, | Over-engineered |
| ERB partials are fine |
, | Use Solid Queue (database-backed) |
| REST with Turbo is sufficient |
Routing: Everything is CRUD
Every action maps to a CRUD verb. Create new resources instead of custom actions:
# Avoid: Custom actions resources :cards do post :close end # Good: State changes as resources 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
Use
scope module: for namespaced nested resources. Use resolve for custom polymorphic URL generation.
Controllers
Thin Controllers, Rich Models
Controllers orchestrate; business logic lives in models.
def create @card.close # All logic in model respond_to do |format| format.turbo_stream { render_card_replacement } format.json { head :no_content } end end
Controller Concerns
Use concerns for shared behavior:
- Resource scoping:
,CardScoped
- load parent resources viaBoardScopedbefore_action - Request context:
- populateCurrentRequest
with request dataCurrent - Security:
,BlockSearchEngineIndexingRequestForgeryProtection - Turbo helpers:
- flash messages via Turbo StreamTurboFlash
Authorization
Check permissions in controller, define permission logic in model:
# Controller before_action :ensure_permission_to_administer_card, only: [:destroy] # Model def can_administer_card?(card) admin? || card.creator == self end
Models & Concerns
Heavy Use of Concerns
Each concern is self-contained with associations, scopes, and methods:
class Card < ApplicationRecord include Assignable, Closeable, Eventable, Pinnable, Watchable end
Concern Structure
module Card::Closeable extend ActiveSupport::Concern included do has_one :closure, dependent: :destroy scope :closed, -> { joins(:closure) } scope :open, -> { where.missing(:closure) } end def closed? = closure.present? def close(user: Current.user) create_closure!(user: user) unless closed? end end
Default Values via Lambdas
belongs_to :account, default: -> { board.account } belongs_to :creator, class_name: "User", default: -> { Current.user }
Current for Request Context
Use
ActiveSupport::CurrentAttributes for session, user, identity, account, and request metadata.
POROs (Plain Old Ruby Objects)
Namespace under parent model:
Event::Description, Card::Eventable::SystemCommenter
Use for:
- Presentation logic - formatting for display
- Complex operations - multi-step processes
- View context bundling - collecting UI state
POROs are model-adjacent, NOT controller-adjacent (that would be a service object).
State as Records, Not Booleans
Create separate records instead of boolean columns:
# Separate record gives you: timestamp, who did it, easy scoping class Closure < ApplicationRecord belongs_to :card, touch: true belongs_to :user, optional: true end card.closure.present? # Is it closed? card.closure.user # Who closed it? card.closure.created_at # When? # Scoping Card.closed # joins(:closure) Card.open # where.missing(:closure)
Examples:
Closure, Pin, Watch, Publication, Goldness
Authentication
Custom passwordless magic link auth (~150 lines). No Devise.
Key components:
concern withAuthentication
,require_authentication
,resume_sessionstart_new_session_for
model (belongs_to identity)Session
model with expiration and consumptionMagicLink- Bearer token authentication for API access
Views & Turbo/Hotwire
- Turbo Streams for partial updates (
,turbo_stream.replace
)turbo_stream.before - Morphing for complex updates (
)method: :morph - Partials over ViewComponents - standard ERB partials with caching
- Stimulus controllers - single-purpose, small (~50 lines),
/static values
for config,static classes
for events,this.dispatch()
for private methodsthis.#privateMethod()
Background Jobs
- Shallow jobs, rich models - jobs just call model methods
and_later
convention -_now
queues job,mark_as_read_later
executes immediatelymark_as_read_now- Solid Queue - database-backed, no Redis
- Recurring jobs via
config/recurring.yml
Testing
- Request specs for controllers (not controller specs)
- Ship tests with features in the same commit
- Use
,change { }
,as: :turbo_streamas: :json
What They Avoid
- No service objects - use model methods
- No form objects (usually) - exception:
as ActiveModelSignup - No decorators/presenters - use view helpers
- No GraphQL - REST with Turbo
Naming Conventions
Methods
- Verbs for actions:
,close
,reopenpublish - Predicates for state:
,closed?published?
Concerns
Adjectives describing capability:
Closeable, Publishable, Watchable, Searchable
Controllers
Nouns matching the resource:
Cards::ClosuresController, Boards::PublicationsController
Scopes
- Ordering:
,chronologically
,reverse_chronologically
,alphabeticallylatest - Preloading:
as standard name for eager loadingpreloaded - Parameterized:
,indexed_by(index)sorted_by(sort)
Caching
HTTP Caching
for conditional GETfresh_when etag: [...]- Global
in ApplicationController (bump to bust caches)etag { "v1" } - Concern-level ETags for timezone, authentication
Fragment Caching
in viewscache card do
for collection renderingcached: true
on associations for cache invalidationtouch: true
Database Patterns
- UUIDs for primary keys
- Every model has
for multi-tenancyaccount_id - URL-based multi-tenancy:
/{account_id}/boards/... - No foreign key constraints - removed for flexibility
CSS Architecture
- Vanilla CSS no Sass, PostCSS, or Tailwind.
- CSS Cascade Layers
@layer reset, base, components, modules, utilities - OKLCH color system with CSS variables
- Modern features
,@starting-style
,color-mix()
, native nesting, container queries:has()
API Design
- Same controllers, different format via
respond_to - Response codes: Create →
+ Location, Update/Delete →201 Created204 No Content - Bearer token authentication
Callbacks
Use sparingly:
for async workafter_commit :relay_later, on: :create
for derived databefore_save :set_defaults- Avoid complex chains, avoid synchronous external calls
Summary
- Start with vanilla Rails - Don't add abstractions until you feel the pain
- Models are rich - Business logic lives in models, not services
- Controllers are thin - Just orchestration and response formatting
- Everything is CRUD - New resource over new action
- State is records - Not boolean columns
- Concerns are compositions - Horizontal behavior sharing
- Build before buying - Auth, search, jobs - all custom
- Database is king - No Redis, no Elasticsearch
- Test with fixtures - Deterministic, fast, simple
- Ship incrementally - Many small commits
- Tests ship with features - Not TDD, not afterthought, but together
- Refactor toward consistency - Establish patterns, then update old code
- CSS uses the platform - Native layers, nesting, OKLCH - no preprocessors
- Design tokens everywhere - CSS variables for colors, spacing, typography
The best code is the code you don't write. The second best is the code that's obviously correct.