37signals Skills

repository·main·Indexed 20 days ago

https://github.com/marckohlbrugge/37signals-skills

A collection of AI agent instructions (skills) for Claude Code, Cursor, and Codex. These skills teach coding assistants to write Ruby on Rails applications following 37signals' architectural patterns, such as rich domain models, Hotwire, and minimal gem usage. Includes specialized skills for Hotwire, Active Job, migrations, security, testing, webhooks, and a 'dhh' review skill, alongside accessibility guides for Rails development.

Tokens
95K
Snippets
286
Records
361
Agent score
71%

What's inside 37signals-skills

  1. Summary of ActionCable Best Practices

    main

    When implementing real-time features in Rails using ActionCable, follow these production-tested principles:

    1. Security first: Always scope broadcasts by tenant/account in multi-tenant apps.
    2. Test connections: Use ActionCable::Connection::TestCase to verify authentication.
    3. Dual broadcasts: Broadcast to both specific resources and catch-all streams for flexibility.
    4. Iterate for broadcasts: Use .each instead of update_all when broadcasts are needed.
    5. Force disconnect: Use remote_connections when permissions change.
    6. Monitor it: Add ActionCable metrics to catch performance issues early.
    7. Test broadcasts: Use assert_turbo_stream_broadcasts to ensure updates work.
    8. Solid Cable: Consider database-backed ActionCable instead of Redis for simpler deployments.
  2. Adopt the 37signals Development Philosophy

    main

    The 37signals development approach prioritizes speed and validation over premature abstraction. Key principles include:

    • Ship, Validate, Refine: Merge "prototype quality" code to validate designs with real usage before performing heavy cleanup.
    • Fix Root Causes, Not Symptoms: Instead of adding retry logic for race conditions, use tools like enqueue_after_transaction_commit. Instead of working around CSRF issues on cached pages, avoid HTTP caching pages that contain forms.
    • Vanilla Rails Over Abstractions: Prefer thin controllers and rich domain models. Avoid service objects unless truly justified; direct ActiveRecord usage (e.g., @card.comments.create!(params)) is encouraged.
    • Write-Time over Read-Time: Perform data manipulation and computations when saving (write-time) rather than during presentation (read-time). Use counter caches and pre-computed roll-ups.
  3. Summary of DHH's Core Code Review Patterns

    main

    A checklist of high-level principles for code quality and design:

    1. Abstractions must earn their keep: If an abstraction doesn't explain or enable variations, inline it.
    2. Write time > Read time: Compute summaries and sort keys when saving (write time), not when presenting (read time).
    3. Database over AR: Prefer database constraints over ActiveRecord validations.
    4. Positive names: Use active instead of not_deleted.
    5. Explicit over clever: Use case statements instead of metaprogramming for 2-3 variations.
    6. StringInquirer for predicates: Use action.completed? instead of manual string comparisons.
    7. Touch chains: Use touch: true for cache invalidation.
    8. Helpers take params: Do not rely on magical instance variables (@ivar) inside helpers; pass parameters explicitly.
    9. Targets over selectors: In Stimulus, use data-*-target instead of complex selectors.
    10. Tests shouldn't shape design: Never add code solely to make it more testable.
  4. Summary of Action Text best practices

    main

    To make Action Text robust and maintainable in production Rails applications, follow these key patterns:

    1. Sanitizer Sync: Always sync sanitizer configuration between Rails and Action Text in production.
    2. Render-time Processing: Process HTML at render time rather than save time to allow for maximum flexibility.
    3. Client-side Enhancements: Use Stimulus for client-side logic like link retargeting.
    4. Remote Images: Use skip_pipeline: true for remote images to prevent processing errors.
    5. Rendering Control: Override attachment partials if you need complete control over how attachments are rendered.
    6. Defensive Styling: Style defensively, as users will inevitably create unexpected HTML structures.
    7. Resilient Testing: Create test helpers to normalize HTML for reliable comparisons.
    8. Edge Case Testing: Explicitly test for malformed attachments and entity-encoded punctuation.
  5. Overview of available 37signals Skills

    main

    The repository provides several specialized skills to guide AI agents in writing Rails code following 37signals patterns.

    Automatic Skill:

    • rails-best-practices-core: The baseline for all Rails work, covering architecture, naming, modeling, REST routing, and authorization.

    On-demand Skills (require explicit invocation): These skills are marked disable-model-invocation: true to prevent bloating every request. Invoke them via slash commands or explicit mentions.

    • rails-hotwire-realtime: Turbo Streams/Frames, Stimulus, ActionCable, etc.
    • rails-jobs: Active Job design, Solid Queue, and retry policies.
    • rails-migrations: Safe schema changes and staged rollouts.
    • rails-security-multitenancy: Path-based tenancy and scoped lookups.
    • rails-testing: Minitest, fixtures, and Turbo assertions.
    • rails-webhooks: Outbox delivery and payload signing.
    • dhh: Review code like DHH (direct, opinionated, anti-over-engineering). Use /dhh to invoke.
  6. Implement the Outbox Pattern for Webhook Delivery

    main

    To ensure reliable webhook delivery, use an outbox pattern where domain events trigger a single dispatch job. This job fans out by creating a persisted Delivery row for every webhook subscription.

    Key implementation details:

    • State Management: Use a state enum for Delivery rows with values: pending, in_progress, completed, and errored.
    • Persistence First: Use after_create_commit :deliver_later on the Delivery model. This ensures the record is persisted before the job is enqueued, allowing state to survive crashes.
    • Crash-Safe Fan-out: Use ActiveJob::Continuable with a cursor-based approach (find_each(start: step.cursor) and step.advance!) to allow mid-batch crashes to resume rather than restart.
    • Auditability: Record request metadata (headers, payload) and response data (status, body) on the Delivery row. Cap response body storage at ~100KB to prevent unbounded growth.
    • Queue Isolation: Use a dedicated webhooks queue to prevent slow destinations from starving other application jobs.
  7. Ensure Environment Variables take precedence over Config

    main

    When configuring application settings, always allow environment variables (ENV) to override static configuration values. This enables runtime overrides in containerized deployments without requiring code changes. Use ENV.fetch to prioritize the environment variable over the existing configuration value.

    # Good - ENV has precedence
    report_uri = ENV.fetch("CSP_REPORT_URI") {
      config.x.content_security_policy.report_uri
    }
  8. Pass explicit parameters to helpers

    main

    Avoid having helpers rely on 'magical' instance variables (like @bubble). Instead, pass the required object as an explicit parameter to make dependencies clear and testing easier.

    Example:

    # Bad - relies on @bubble ivar
    def bubble_activity_count
      @bubble.comments_count + @bubble.events_count
    end
    
    # Good - explicit dependency
    def bubble_activity_count(bubble)
      bubble.comments_count + bubble.events_count
    end
  9. Reclaim container padding with negative margins on mobile

    main

    To maximize screen real estate on mobile, you can break out of a container's padding by using negative margins that match the container's padding. Use CSS variables to ensure the padding and negative margin values stay synchronized.

    Implementation: Set the inline-size to 100% + 2 * padding and apply a negative margin-inline equal to the padding value.

    @media (max-width: 800px) {
      .card-perma__container {
        --padding-inline: var(--main-padding);
    
        inline-size: calc(100% + 2 * var(--padding-inline));
        margin-inline: calc(-1 * var(--padding-inline));
        max-inline-size: none;
      }
    }
  10. Implement sharded search using MySQL

    main

    Instead of using Elasticsearch, use MySQL shards for full-text search. This keeps operations simple by using a single database technology. You can distribute data across shards using a hashing mechanism (like Zlib.crc32) based on the account_id.

    class Search::Record < ApplicationRecord
      connects_to shards: {
        shard_0: { writing: :search_0, reading: :search_0 },
        shard_1: { writing: :search_1, reading: :search_1 },
        # ...
      }
    
      def self.shard_for(account)
        :"shard_#{Zlib.crc32(account.id.to_s) % 16}"
      end
    end
  11. Bundle notifications using time windows

    main

    To avoid complex many-to-many relationships, bundle notifications into time windows using a Notification::Bundle model. Instead of linking notifications to bundles via foreign keys, query notifications dynamically based on whether their created_at timestamp falls within the bundle's starts_at and ends_at range. This design keeps notifications immutable and the schema lightweight.

    class Notification::Bundle < ApplicationRecord
      belongs_to :user
    
      enum :status, %i[ pending processing delivered ]
    
      scope :due, -> { pending.where("ends_at <= ?", Time.current) }
      scope :containing, ->(notification) {
        where("starts_at <= ? AND ends_at > ?", notification.created_at, notification.created_at)
      }
    
      # Query notifications in the window dynamically - no foreign keys needed!
      def notifications
        user.notifications.where(created_at: window).unread
      end
    
      private
        def window
          starts_at..ends_at
        end
    end