Use when writing Rails models - enforces state-as-records not booleans, concerns as adjectives namespaced under model, invocation ordering, and private indentation
View on GitHubZempTime/zemptime-marketplace
vanilla-rails
January 20, 2026
Select agents to install to:
npx add-skill https://github.com/ZempTime/zemptime-marketplace/blob/main/vanilla-rails/skills/models/SKILL.md -a claude-code --skill vanilla-rails-modelsInstallation paths:
.claude/skills/vanilla-rails-models/# Vanilla Rails Models
Rich domain models with concerns, following production Basecamp/37signals patterns. Decompose with concerns, not services.
## State as Separate Records (NOT Booleans)
**Critical pattern:** Don't use boolean columns for state. Create state records that capture who/when.
```ruby
# ❌ BAD - boolean column
class AddStarredToCards < ActiveRecord::Migration[7.0]
def change
add_column :cards, :starred, :boolean, default: false
add_column :cards, :starred_at, :datetime
end
end
class Card < ApplicationRecord
def star
update(starred: true, starred_at: Time.current)
end
end
# ✅ GOOD - state record
class CreateStars < ActiveRecord::Migration[7.0]
def change
create_table :stars, id: :uuid do |t|
t.references :card, null: false, foreign_key: true, type: :uuid
t.references :user, null: false, foreign_key: true, type: :uuid
t.timestamps
end
end
end
class Star < ApplicationRecord
belongs_to :card
belongs_to :user
end
class Card < ApplicationRecord
has_one :star, dependent: :destroy
def star(user: Current.user)
create_star!(user: user) unless starred?
end
def starred?
star.present?
end
end
```
**Why state records:**
- Captures who (user) and when (created_at) automatically
- Can add metadata later without altering main table
- Explicit presence/absence vs true/false ambiguity
- Database enforces referential integrity
**Use has_one for binary state, has_many for multi-user:**
```ruby
# Binary state (one per item) - use has_one
has_one :closure # card is either closed or not
has_one :triage # card is either triaged or not
# Multi-user actions - use has_many
has_many :pins # multiple users can pin
has_many :watches # multiple users can watch
has_many :assignments
```
**Common rationalizations to reject:**
| Excuse | Reality |
|--------|---------|
| "Boolean is simpler" | State records capture metadata you'll need later |
| "Just a flag" | Today's flag is tomorrow'sIssues Found: