Principles

When changing this codebase, make sure:

  • it has 90% code coverage for backend code
  • all tests passing
  • meaningful commit messages along the way

Rails commands

bin/rails is the command to run rails

Admin generation

To generate an admin interface for a model, use the following command:

bin/rails generate admin:scaffold ModelName field1:type field2:type ...

Testing Configuration

  • Use FactoryBot for test data
  • We must have a valid factory for every model we want to test
  • We should have a test for every model

General Architecture

  • Write a good controller test for any feature that touches a controller. Consider things like the authenticated state of the user, their ability to perform certain actions, their verified status.
  • We should have system tests for crucial flows, but not every feature should be tested via system tests.

Turbo Conventions

  • UI relies on Turbo for most interactions, giving an SPA feel for the app
  • Follow “Convention over Configuration” for Turbo interactions
  • Use consistent Turbo Frame IDs based on the resource relationship hierarchy
  • Avoid custom turbo_stream operations when standard redirects can work

Turbo Frame Naming Conventions

Use the frame_id_from_path helper to derive Turbo frame IDs directly from Rails path helpers:

# For a list of items
<%= turbo_frame_tag frame_id_from_path(consolidation_consignment_items_path(consolidation, consignment)) %>
 
# For a new item form
<%= turbo_frame_tag frame_id_from_path(new_consolidation_consignment_item_path(consolidation, consignment)) %>
 
# For a specific item
<%= turbo_frame_tag frame_id_from_path(consolidation_consignment_item_path(consolidation, consignment, item)) %>
 
# In links pointing to a frame
<%= link_to "Edit", 
            edit_consolidation_consignment_item_path(consolidation, consignment, item),
            data: { turbo_frame: frame_id_from_path(consolidation_consignment_item_path(consolidation, consignment, item)) } %>
            
# For delete buttons and cancel links, target the parent collection frame (avoids "Content missing" errors)
<%= button_to delete_path(resource),
            method: :delete,
            form: { data: { turbo_frame: frame_id_from_path(collection_path) } } do %>
  <%= lucide_icon "trash-2" %>
<% end %>
 
# For cancel links in new/edit forms
<%= link_to "Cancel", collection_path %>

This helper automatically converts a Rails path to a consistent turbo frame ID by replacing slashes with underscores:

  • /consolidations/1/consignments/2/items becomes consolidations_1_consignments_2_items
  • /consolidations/1/consignments/2/items/3/edit becomes consolidations_1_consignments_2_items_3_edit

Benefits:

  • Frame IDs always match your routes
  • If routes change, frame IDs automatically update
  • Eliminates manually synchronized IDs
  • Creates a predictable pattern across the application

Lazy Loading Content with RESTful Controllers

Use the src attribute with turbo frames to lazy load content directly from controllers, avoiding the need for custom partials and explicit renders:

# Lazy load the entire items list from the items controller
<%= turbo_frame_tag frame_id_from_path(consolidation_consignment_items_path(consolidation, consignment)), 
    src: consolidation_consignment_items_path(consolidation, consignment),
    loading: "lazy" do %>
    <div class="bg-white rounded-md shadow p-4 text-center text-gray-500 text-sm">
      <p>Loading items...</p>
    </div>
<% end %>

Benefits:

  • Keeps views cleaner without embedded partials
  • Allows controllers to handle their own rendering
  • Maintains RESTful convention
  • Reduces initial page load time
  • Provides loading states for better UX

RESTful Controller and Turbo Frame Hierarchy Conventions

  • Controllers must remain RESTful and lean - avoid turbo_stream-specific logic
  • Always use standard redirects (no specialized turbo_stream renders)
  • For nested resources, redirect to the collection path (items_path), not the parent path
  • Never create format-specific response blocks unless absolutely necessary
  • Let the frame_id_from_path helper and proper routing handle frame updates automatically

Frame Hierarchy Design Pattern

Structure your Turbo Frames to match the RESTful hierarchy:

Controller#index (Main frame: items_path)
├── List of items
└── New item form (Target: new_item_path)

Benefits:

  • Clean parent-child relationship between frames
  • Navigation (links/redirects) uses standard RESTful routes
  • All UI for a resource is managed by its controller
  • Redirecting to index (collection path) resets UI state
  • Cancel operations simply load index in the same frame
# GOOD - Simple, consistent redirects to collection path
def create
  @item = @parent.items.build(item_params)
  if @item.save
    redirect_to parent_items_path(@parent), notice: "Item created"
  else
    render :new, status: :unprocessable_entity
  end
end
 
# Form submissions must target parent frame to maintain frame hierarchy
<%= simple_form_for @item, 
  url: parent_items_path(@parent),
  data: { 
    turbo: true,
    turbo_frame: frame_id_from_path(parent_items_path(@parent)) # Point to parent frame
  } do |f| %>
  <!-- Form fields -->
<% end %>
 
# BAD - Don't use custom turbo_stream rendering
def create
  # ...
  respond_to do |format|
    format.html { redirect_to parent_path }
    format.turbo_stream { 
      render turbo_stream: turbo_stream.replace(...)
    }
  end
end

Additional Turbo Tips

  • Think twice before using turbo_streams, as they can be complex; prefer turbo frames when possible
  • Remember, table rows can’t be wrapped in turbo frames directly - use a grid-based approach or consider building a proper table component
  • For deeply nested or complex UI interactions, consider using Stimulus controllers to enhance Turbo Frames

Other architecture

  • Using PostgreSQL for our database
  • ActiveJob for background jobs
  • Where possible encapuslate business logic in services
    • I like the pattern where services return a hash with a success boolean and a message

UI Styling

  • We use simple_form for our forms. You can use the simple_form_for helper to generate a form with our custom form builder. check docs/form_layout.md for info about how to build forms
  • If you are styling a form, you don’t need to add CSS classes. You can rely on our custom form builder to handle the styling for you
  • We use Tailwind CSS for our styling
  • we use lucide-rails for icons, you can use the lucide_icon helper to render an icon

Search Implementation

  • Use Ransack gem for implementing search functionality
  • For security, always restrict search to current user’s organization
  • Configure models with proper ransackable_attributes and ransackable_associations methods
  • Do not expose organization_id as a searchable attribute for security reasons
  • For search forms, use the native Ransack form builder methods (not simple_form):
    • search_field for text search
    • select for dropdowns with options_for_select
  • Add proper test IDs to search forms:
    • id="search-field-name" for search fields
    • id="submit-search" for the submit button
    • id="clear-search" for the clear button
  • Always include a clear button for search forms with active filters
  • Always test search functionality in both controller and system tests
  • For more complex search needs, consider adding custom ransackers to the model
  • See docs/form_layouts.md for detailed examples of search form implementation