Rails 8 Apps skill for Codex

Omakase Rails: REST, Hotwire and Solid Queue done right. Codex builds Rails 8 features along the framework's grain: resourceful routes, params.expect, lookups through the current user's associations, and validations mirrored by database constraints. It reaches for Turbo Frames, Streams and morph refreshes before custom JavaScript, enqueues idempotent Solid Queue jobs after commit, and finishes with tests, RuboCop, Brakeman and a migration rollback check. Kamal deploys wait for approval.

  • Backend & APIs
  • $rails-apps
  • 234 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- rails-apps

Installs to ~/.agents/skills/rails-apps/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $rails-apps, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Rails 8 applications the omakase way - RESTful resources, params.expect strong parameters, Active Record models with validations backed by database constraints, eager loading and strict_loading, the built-in authentication generator, Hotwire Turbo Frames, Streams and morphing refreshes with Stimulus, Solid Queue jobs and recurring tasks, Solid Cache and Cable, and Minitest. Use when adding a Rails feature, model or controller, wiring Turbo or Stimulus, moving jobs to Solid Queue, or upgrading an app to Rails 8.

Use it when

  • Adding models, controllers and views to a Rails app
  • Making pages interactive with Turbo Frames, Streams, morphing or Stimulus
  • Setting up Solid Queue jobs or recurring tasks
  • Upgrading an application to Rails 8

Use something else when

How to check the result

bin/ci or the test suite, RuboCop and Brakeman pass, migrations roll back and reapply cleanly, and strict_loading raises no lazy-load errors on the new pages.

The mistake it prevents

Finding records with Order.find(params[:id]) instead of through Current.user.orders, which exposes other users' data by guessing ids.

~/.agents/skills/rails-apps/SKILL.md

--- name: rails-apps description: Builds Rails 8 applications the omakase way - RESTful resources, params.expect strong parameters, Active Record models with validations backed by… license: MIT ---

Done well means: the feature fits Rails conventions (resourceful routes, fat-enough models, thin controllers), every validation that matters is also a database constraint, pages render without N+1 queries, interactivity comes from Turbo before custom JavaScript, slow work runs in jobs enqueued after commit, and bin/rails test, RuboCop and Brakeman pass.

Inspect first

  • Read AGENTS.md, Gemfile/Gemfile.lock (Rails version, solid_queue, turbo-rails, stimulus-rails, importmap-rails or jsbundling), config/database.yml (separate queue/cache/cable databases in Rails 8), config/routes.rb.
  • Check for bin/ci (Rails 8.1 local CI, defined in config/ci.rb); if present it is the definition of “all checks”.
  • bin/rails routes -g orders and bin/rails db:migrate:status before changing anything.

Generators, then trim

bin/rails generate model Order customer:references status:string total_cents:integer
bin/rails generate controller Orders index show new create
bin/rails generate authentication        # Rails 8: User, Session, Authentication concern

Generators are a starting point. Remove unused actions, views and tests they create; never commit scaffold code you have not read.

Routes and controllers

# config/routes.rb
resources :orders, only: %i[index show new create] do
  resource :cancellation, only: :create      # POST /orders/:order_id/cancellation
end

Model verbs as nested singular resources (cancellation) instead of custom member actions; keeps controllers to the seven REST actions.

class OrdersController < ApplicationController
  rate_limit to: 10, within: 1.minute, only: :create

  def index
    @orders = Current.user.orders.includes(:line_items).order(created_at: :desc).limit(50)
  end

  def show
    @order = Current.user.orders.find(params.expect(:id))
  end

  def create
    @order = Current.user.orders.build(order_params)
    if @order.save
      redirect_to @order, notice: "Order placed"
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def order_params
    params.expect(order: [:shipping_address, :notes, line_items_attributes: [[:sku, :qty]]])
  end
end
  • params.expect (Rails 8) replaces require(...).permit(...) and returns 400 instead of raising 500 when the shape is wrong. Arrays of hashes use double brackets.
  • Always find through the association (Current.user.orders.find), never Order.find(params[:id]) for user-owned data.
  • Render failed forms with status: :unprocessable_entity (422) or Turbo will not replace the page.
  • With the authentication generator, controllers require login by default; opt out with allow_unauthenticated_access only: %i[index].

Models

class Order < ApplicationRecord
  belongs_to :customer, class_name: "User"
  has_many :line_items, dependent: :destroy
  accepts_nested_attributes_for :line_items

  enum :status, { pending: "pending", paid: "paid", cancelled: "cancelled" }, default: :pending, validate: true
  normalizes :shipping_address, with: ->(a) { a.strip }

  validates :total_cents, numericality: { only_integer: true, greater_than_or_equal_to: 0 }

  after_create_commit -> { OrderMailer.with(order: self).placed.deliver_later }
end
class CreateOrders < ActiveRecord::Migration[8.0]
  def change
    create_table :orders do |t|
      t.references :customer, null: false, foreign_key: { to_table: :users }
      t.string :status, null: false, default: "pending"
      t.integer :total_cents, null: false, default: 0
      t.timestamps
    end
    add_index :orders, [:customer_id, :created_at]
    add_check_constraint :orders, "total_cents >= 0", name: "orders_total_nonnegative"
  end
end
  • Uniqueness validations race; back them with a unique index.
  • Side effects (mail, jobs, broadcasts) go in after_commit/after_create_commit, not after_save, so they never fire for rolled-back transactions.
  • String-backed enums survive reordering; integer enums break if values shift.
  • Large-table migrations under load: add indexes with algorithm: :concurrently and disable_ddl_transaction! on PostgreSQL; the strong_migrations gem catches unsafe operations. See $zero-downtime-migrations.

Queries

  • Eager load what views render: includes(:line_items); preload / eager_load when you need to force the strategy.
  • Turn on strict_loading per association or config.active_record.strict_loading_by_default in development and test to make lazy loads raise.
  • Batch work with find_each/in_batches, never .all.each on big tables.
  • Deeper tuning: $active-record-performance.

Hotwire

NeedTool
Update part of a page from a link or formTurbo Frame (turbo_frame_tag)
Several regions change after a form submitTurbo Stream response (create.turbo_stream.erb)
Live updates pushed to other usersbroadcasts_refreshes + turbo_stream_from, or targeted broadcasts
Whole page refresh without losing scrollTurbo 8 morphing: turbo_refreshes_with method: :morph, scroll: :preserve in the layout
Small client behaviour (toggle, copy, debounce)Stimulus controller
<%# app/views/orders/show.html.erb %>
<%= turbo_stream_from @order %>
<%= turbo_frame_tag dom_id(@order, :status) do %>
  <%= render "orders/status", order: @order %>
<% end %>
class Order < ApplicationRecord
  broadcasts_refreshes      # pages subscribed with turbo_stream_from @order refresh (morph) on change
end

Broadcasts run over Action Cable (Solid Cable by default in Rails 8). Authorize stream subscriptions: turbo_stream_from uses signed stream names, so only render it for users allowed to see the record.

Jobs with Solid Queue

class SyncOrderJob < ApplicationJob
  queue_as :default
  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
  discard_on ActiveJob::DeserializationError

  def perform(order)
    ExternalErp.sync(order)   # must be idempotent; jobs can run more than once
  end
end
  • Rails 8 sets config.active_job.queue_adapter = :solid_queue in production, backed by the queue database. Run workers with bin/jobs, or inside Puma via plugin :solid_queue (enabled with SOLID_QUEUE_IN_PUMA) for small deployments.
  • Recurring tasks live in config/recurring.yml.
  • perform_later inside a transaction may enqueue before commit on other adapters; Solid Queue in the same database is transactional, but in a separate queue database it is not. Enqueue from after_commit callbacks to be safe.
  • Mission Control Jobs (mission_control-jobs gem) gives a UI for failed jobs; mount it behind admin auth.

Testing

bin/rails test                     # models, controllers, integration
bin/rails test:system              # browser tests, if the app keeps them
bin/rubocop
bin/brakeman --no-pager

Integration tests (ActionDispatch::IntegrationTest) for each controller path: success, validation failure (422), another user’s record (404), unauthenticated (redirect to sign in). Use assert_enqueued_with(job: SyncOrderJob) for jobs and assert_turbo_stream action: :replace from turbo-rails test helpers for stream responses.

Deploying

Rails 8 ships Kamal 2 config (config/deploy.yml) and Thruster in the Dockerfile. bin/kamal deploy pushes to production servers: show the user the target and wait for explicit confirmation before running it. Secrets come from .kamal/secrets and bin/rails credentials:edit; never commit config/master.key.

Upgrading to Rails 8

  1. Get on the latest 7.2 patch with zero deprecation warnings first.
  2. Bump rails, run bin/rails app:update, review each config diff instead of accepting all.
  3. Move config.load_defaults up one version at a time using new_framework_defaults_8_0.rb.
  4. Solid Queue, Cache and Cable, Propshaft and the auth generator are optional for upgraded apps; adopt them in separate changes.

Verification

Run bin/ci if it exists; otherwise bin/rails db:migrate (and db:rollback + db:migrate once to prove reversibility), bin/rails test, bin/rubocop and bin/brakeman. Open the page in bin/dev and watch the log for N+1 warnings from strict_loading.

Output template

## Rails change: <summary>
- Routes: <resources added>
- Models/migrations: <tables, constraints, indexes>
- Hotwire: <frames/streams/broadcasts>
- Jobs: <job, queue, retry policy>
- Checks: tests, rubocop, brakeman, migration rollback

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Rails 8 Apps"
  short_description: "Omakase Rails: REST, Hotwire and Solid Queue done right"
  default_prompt: "$rails-apps Add an orders feature to our Rails 8 app with live status updates via Turbo"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem