All skills
sergiodxa avatar

/ruby-on-rails-best-practices

@102fba6

Ruby on Rails architecture and coding patterns from Basecamp. Use when writing, reviewing, or refactoring Rails code to follow proven conventions for models, controllers, jobs, and concerns. Triggers on tasks involving Rails models, concerns, controllers, background jobs, or Turbo/Hotwire.

Use this Skill: https://skilld.dev/gh/sergiodxa/agent-skills/ruby-on-rails-best-practices

This session only. Nothing lands on disk.

rulesturbo-broadcasts.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Turbo Stream Broadcast Patterns

Encapsulate broadcast logic in model concerns and call broadcasts explicitly from controllers. Use composite stream names for targeting specific audiences.

Why

  • Explicit control: Broadcasts happen when you intend, not automatically
  • Testability: Broadcasts can be tested in isolation
  • Flexibility: Different contexts may need different broadcast behavior
  • Performance: No unexpected broadcasts on every save

Pattern 1: Broadcast Concerns in Models

Encapsulate broadcast logic in a model concern:

# app/models/message/broadcasts.rb
module Message::Broadcasts
  def broadcast_create
    broadcast_append_to room, :messages,
      target: [room, :messages]
    ActionCable.server.broadcast("unread_rooms", { roomId: room.id })
  end

  def broadcast_update
    broadcast_replace_to room, :messages,
      target: [self, :presentation],
      partial: "messages/presentation",
      attributes: { maintain_scroll: true }
  end

  def broadcast_remove
    broadcast_remove_to room, :messages
  end
end

# app/models/message.rb
class Message < ApplicationRecord
  include Broadcasts
end

Pattern 2: Call Broadcasts from Controllers

Explicitly call broadcasts in controller actions:

# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def create
    @message = @room.messages.create!(message_params)
    @message.broadcast_create
  end

  def update
    @message.update!(message_params)
    @message.broadcast_update
  end

  def destroy
    @message.destroy
    @message.broadcast_remove
  end
end

Pattern 3: Composite Stream Names

Use arrays for stream names to create hierarchical channels:

<%# Subscribe to room-specific messages %>
<%= turbo_stream_from @room, :messages %>

<%# Subscribe to global room list %>
<%= turbo_stream_from :rooms %>

<%# Subscribe to user-specific room updates %>
<%= turbo_stream_from Current.user, :rooms %>

<%# Subscribe to card activity %>
<%= turbo_stream_from @card, :activity %>

This generates stream names like:

  • "Z2lkOi8vYXBwL1Jvb20vMQ:messages" (room + messages)
  • "rooms" (global)
  • "Z2lkOi8vYXBwL1VzZXIvMQ:rooms" (user + rooms)

Pattern 4: Targeted Broadcasts by Audience

Different users may need different broadcasts:

# app/controllers/rooms/opens_controller.rb
class Rooms::OpensController < RoomsController
  def create
    room = Rooms::Open.create!(room_params)
    broadcast_create_room(room)
  end

  private
    # Open rooms: broadcast to everyone
    def broadcast_create_room(room)
      broadcast_prepend_to :rooms,
        target: :shared_rooms,
        partial: "sidebars/room",
        locals: { room: room }
    end
end

# app/controllers/rooms/closeds_controller.rb
class Rooms::ClosedsController < RoomsController
  private
    # Closed rooms: broadcast only to members
    def broadcast_create_room(room)
      html = render_to_string(partial: "sidebars/room", locals: { room: room })
      room.users.each do |user|
        broadcast_prepend_to user, :rooms,
          target: :shared_rooms,
          html: html  # Render once, broadcast to many
      end
    end
end

Pattern 5: Controller Broadcast Helpers

Include Turbo broadcast modules in ApplicationController:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Turbo::Streams::Broadcasts
  include Turbo::Streams::StreamName
end

Then broadcast directly from controller actions:

class RoomsController < ApplicationController
  def destroy
    @room.destroy
    broadcast_remove_to :rooms, target: [@room, :list]
  end
end

Pattern 6: Broadcast with morph

Use method: :morph for smart DOM updates:

def broadcast_card_update
  broadcast_replace_to @board,
    target: [@card, :card_container],
    partial: "cards/container",
    method: :morph,
    locals: { card: self }
end

Pattern 7: Custom Attributes

Pass custom attributes for client-side handling:

def broadcast_update
  broadcast_replace_to room, :messages,
    target: [self, :presentation],
    partial: "messages/presentation",
    attributes: { maintain_scroll: true }  # Custom attribute
end

Handle in JavaScript:

// app/javascript/controllers/maintain_scroll_controller.js
beforeStreamRender(event) {
  if (event.detail.newStream.hasAttribute("maintain_scroll")) {
    // Preserve scroll position
  }
}

Pattern 8: Conditional Broadcasting

Broadcast based on context:

module Card::Broadcastable
  def broadcast_changes
    return unless published?

    broadcast_replace_to board,
      target: [self, :card_container],
      partial: "cards/container",
      method: :morph
  end
end

Turbo Stream Template Organization

app/views/
├── cards/
│   ├── closures/
│   │   ├── create.turbo_stream.erb
│   │   └── destroy.turbo_stream.erb
│   ├── comments/
│   │   ├── create.turbo_stream.erb
│   │   └── update.turbo_stream.erb
│   └── update.turbo_stream.erb

Example template:

<%# app/views/cards/closures/create.turbo_stream.erb %>
<%= turbo_stream.replace(
  [@card, :card_container],
  partial: "cards/container",
  method: :morph,
  locals: { card: @card.reload }
) %>

<% if @source_column %>
  <%= turbo_stream.replace(
    dom_id(@source_column),
    partial: "columns/column",
    method: :morph,
    locals: { column: @source_column }
  ) %>
<% end %>

When NOT to Use Callbacks for Broadcasts

# Bad: Broadcasts on every save, even in background jobs
after_save_commit :broadcast_changes

# Good: Explicit broadcast when needed
# Called from controller:
@card.update!(card_params)
@card.broadcast_changes

Rules

  1. Encapsulate broadcast logic in model concerns
  2. Call broadcasts explicitly from controllers
  3. Use composite stream names ([room, :messages]) for scoping
  4. Render once, broadcast to many for multi-user broadcasts
  5. Use method: :morph for smart DOM updates
  6. Don't use callbacks for broadcasts (be explicit)
  7. Custom attributes can signal client-side behavior

Source: SKILL.md on GitHub

1 warning17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides architectural guidelines and coding patterns for Ruby on Rails development based on Basecamp's practices. No security issues were detected.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    17/17 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 102fba6. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago

README badge

README badge for sergiodxa/agent-skills/ruby-on-rails-best-practices