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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- rails-apps Installs to ~/.agents/skills/rails-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install rails-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project rails-apps Installs to .agents/skills/rails-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/rails-apps.zipunzip -o rails-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) rails-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The problem is slow Active Record queries: use active-record-performance
- You are deploying with Kamal and need server setup detail: use kamal-deploys
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.
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-railsor jsbundling),config/database.yml(separatequeue/cache/cabledatabases in Rails 8),config/routes.rb. - Check for
bin/ci(Rails 8.1 local CI, defined inconfig/ci.rb); if present it is the definition of “all checks”. bin/rails routes -g ordersandbin/rails db:migrate:statusbefore 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) replacesrequire(...).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), neverOrder.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, notafter_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: :concurrentlyanddisable_ddl_transaction!on PostgreSQL; thestrong_migrationsgem catches unsafe operations. See$zero-downtime-migrations.
Queries
- Eager load what views render:
includes(:line_items);preload/eager_loadwhen you need to force the strategy. - Turn on
strict_loadingper association orconfig.active_record.strict_loading_by_defaultin development and test to make lazy loads raise. - Batch work with
find_each/in_batches, never.all.eachon big tables. - Deeper tuning:
$active-record-performance.
Hotwire
| Need | Tool |
|---|---|
| Update part of a page from a link or form | Turbo Frame (turbo_frame_tag) |
| Several regions change after a form submit | Turbo Stream response (create.turbo_stream.erb) |
| Live updates pushed to other users | broadcasts_refreshes + turbo_stream_from, or targeted broadcasts |
| Whole page refresh without losing scroll | Turbo 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_queuein production, backed by thequeuedatabase. Run workers withbin/jobs, or inside Puma viaplugin :solid_queue(enabled withSOLID_QUEUE_IN_PUMA) for small deployments. - Recurring tasks live in
config/recurring.yml. perform_laterinside a transaction may enqueue before commit on other adapters; Solid Queue in the same database is transactional, but in a separatequeuedatabase it is not. Enqueue fromafter_commitcallbacks to be safe.- Mission Control Jobs (
mission_control-jobsgem) 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
- Get on the latest 7.2 patch with zero deprecation warnings first.
- Bump
rails, runbin/rails app:update, review each config diff instead of accepting all. - Move
config.load_defaultsup one version at a time usingnew_framework_defaults_8_0.rb. - 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.yaml
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.