๐Ÿ”ฅ

Campfire (37signals)

The DHH style textbook โ€” real-time chat built with only Rails built-ins

GitHub: basecamp/once-campfire

Campfire is a self-hosted chat app released under 37signals' ONCE brand. It implements real-time chat using only Rails built-in features, making it the most complete practical example of the DHH coding style.

Please refer to the Korean version for the detailed directory structure and pattern analysis, including: CRUD controllers with zero custom actions, state management via records instead of booleans, authentication without Devise using has_secure_password, Concern separation to prevent model bloat, the Current pattern, and 6 dedicated Action Cable channels.

The ONCE philosophy is directly reflected in the code: SQLite by default, Solid Queue/Cache/Cable, email/password without OAuth, single-server deployment with Kamal.

Architecture Diagram

CRUD Mapping Pattern

Typical approach (custom actions)
RoomsController
def index ... end
def show ... end
def create ... end
def close ... end
def reopen ... end
def archive ... end
def ban_user ... end
7+ actions = Fat Controller
Campfire approach (CRUD separation)
RoomsController index / show / create
Rooms::ClosedsController create / destroy
Messages::BoostsController create / destroy
Users::BansController create / destroy
Each controller only has CRUD = Skinny Controllers

State = Record (ERD)

๐Ÿ‘ค User has_secure_password
id, email_address, password_digest, name, role
has_many
has_many
has_many
๐Ÿšซ
user_id
administrator_id
created_at
Exists = Banned
โšก
message_id
user_id
content
Exists = Boosted
๐Ÿ”—
room_id
user_id
created_at
Exists = Joined
Key point: Instead of <strong>is_banned: boolean</strong>, determine state by <strong>Ban record existence</strong>
Naturally records who changed what and when

Concern Separation Structure

๐Ÿ‘ค User Model concerns/ ↗
include User::Bannable Ban
include User::Mentionable @Mention
include User::Role Role
include User::Transferable Transfer
๐Ÿ’ฌ Message Model concerns/ ↗
include Message::Broadcasts Cable
include Message::Pagination Page
include Message::Searchable Search

Action Cable Channel Structure

Key Points

1

Open basecamp/once-campfire repository on GitHub

2

config/routes.rb โ†’ check resource nesting and CRUD mapping patterns

3

app/controllers/rooms/ โ†’ analyze closeds, opens, directs controllers

4

app/models/user.rb โ†’ verify has_secure_password auth (no Devise)

5

app/models/user/*.rb โ†’ learn Concern separation patterns

6

app/models/ban.rb, boost.rb โ†’ check state-as-record management pattern

7

app/channels/ โ†’ analyze 6 Action Cable channel structures

8

app/javascript/controllers/ โ†’ check Stimulus controller patterns

9

Apply patterns one by one to your own project

Pros

  • The most complete real-world example of DHH style
  • CRUD mapping patterns verifiable in actual code
  • Understand the right scale of Concern separation
  • Reference 6 production Action Cable channel implementations
  • Experience the simple architecture of the ONCE philosophy
  • ~30 production Stimulus controller patterns

Cons

  • Single-tenant, difficult to directly apply to multi-tenant apps
  • Small-scale chat app, differs from large-scale services
  • Minitest + Fixtures โ€” may be unfamiliar to RSpec users
  • Native CSS โ€” differs from Tailwind trend
  • ONCE license limits forking/modification

Use Cases

CRUD mapping: rooms/closeds_controller.rb โ€” close room = create, reopen = destroy CRUD mapping: messages/boosts_controller.rb โ€” boost = create/destroy CRUD mapping: users/bans_controller.rb โ€” ban = create/destroy Record state: Ban, Boost, Membership โ€” records instead of booleans Concern separation: User::Bannable, Message::Broadcasts, etc. Current pattern: request-scoped global state management Auth without Devise: has_secure_password + Authentication concern