Rails + Hotwire (Stimulus + Turbo)
Server-rendered frontends with Turbo Frames, Streams, and Stimulus.
# CLAUDE.md — Rails + Hotwire (Stimulus + Turbo)
## Mental model
- Server renders HTML. **Turbo** intercepts navigation and form submits, swaps fragments without a full reload. **Stimulus** sprinkles behavior onto rendered HTML.
- Don't reach for React/Vue. The whole point of Hotwire is staying in Rails.
- Default to Turbo Drive for navigation, Turbo Frames for partial updates, Turbo Streams for live updates from the server.
## Turbo Frames
- Wrap a region in `<turbo-frame id="...">`. Links and forms inside the frame replace the frame's content with the matching frame in the response.
- Frame IDs are stable strings, ideally tied to a model: `dom_id(@invoice)`, `"new_user"`.
- Don't nest frames unless you really need scoped navigation. They get confusing fast.
## Turbo Streams
- Stream updates from the server push to multiple clients via Action Cable:
```ruby
# in the model
after_create_commit -> { broadcast_append_to "invoices", target: "invoices_list" }
```
- Stream actions: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`. Pick the smallest one that does the job.
- Wire up the channel in the view: `<%= turbo_stream_from "invoices" %>`.
## Turbo Drive caveats
- Turbo caches pages aggressively. Flag pages that shouldn't cache: `<meta name="turbo-cache-control" content="no-cache">`.
- For full reloads (auth flows, marketing pages), opt out: `<a data-turbo="false">`.
- `turbo:load` event runs on every navigation. `turbo:before-cache` to clean up before a snapshot.
## Stimulus
- One controller per behavior. Files in `app/javascript/controllers/`.
- Controller classes match HTML data attributes:
```html
<div data-controller="modal" data-modal-open-value="false">
<button data-action="click->modal#toggle">Open</button>
<div data-modal-target="content">...</div>
</div>
```
- Use **values**, **targets**, **classes**, **outlets** — not arbitrary `dataset` reads.
- Don't write a controller for a single use. Inline JS or a Turbo response is often enough.
## Forms
- `form_with` uses Turbo by default. Server returns either a redirect (success), a re-rendered form (validation), or a Turbo Stream (in-place update).
- For multi-step interactions, use Turbo Frames with frame-specific responses — keeps the URL stable while the user works inside.
## Action Cable
- Streams piggyback on Action Cable. Configure Redis (or Solid Cable on Rails 8+) and you're done.
- Authenticate channels via `connection.identified_by :current_user` in `ApplicationCable::Connection`.
- Don't broadcast PII to public channels. Each user gets their own stream key when needed.
## Don't
- Don't ship a Turbo response that contains `<head>` tags. They'll be ignored, and you'll be confused why.
- Don't build a Turbo Stream by string-concatenating HTML. Use the `turbo_stream` helpers — they escape correctly.
- Don't use `data-turbo-method="delete"` on an `<a>` for destructive actions. Use `button_to` so it's a real form.
- Don't put business logic in Stimulus. Stimulus is for UI behavior; data and decisions live on the server.
Other Ruby on Rails templates
Modern Rails Rules
Rails 7+ best practices: conventions, generators, and the Omakase stack.
Rails ActiveRecord Patterns
Model design, scopes, callbacks, concerns, N+1 prevention.
Rails Testing with RSpec
RSpec setup, factories, system specs, request specs, and fast suites.
Rails Deployment (Kamal + Docker)
Deploy Rails with Kamal, Docker, and the Solid Queue/Cable stack.