원클릭으로
new-app
Scaffold a new Rails app with event sourcing, following project conventions (todomvc as reference)
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Scaffold a new Rails app with event sourcing, following project conventions (todomvc as reference)
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Build a new read model following project conventions (event handlers, tests, migration, configuration)
Plan a new feature end-to-end — impact analysis across all layers before delegating to /domain, /read-model, /controller skills
Create atomic git commits following project conventions
Upgrade Rails framework to a newer version following the smooth upgrade methodology
Upgrade RailsEventStore (RES) gems to a newer version
Create a new domain bounded context with aggregates, commands, events, and handlers
| name | new-app |
| description | Scaffold a new Rails app with event sourcing, following project conventions (todomvc as reference) |
Use this skill when asked to create a new Rails application in the apps/ directory. Each app is a standalone Rails application that uses domain modules from domains/ and the shared infra gem.
apps/todo_mvc is the canonical reference for app structure (initializer, Configuration, test harness, Makefile). For the current RES 3.0 wiring and gem versions, use apps/rails_application and apps/twitter — todo_mvc still pins RES 2.x, which is stale. When the two disagree, follow rails_application.
Always scaffold on the newest Ruby and newest Rails — do not hardcode versions from this doc, they go stale.
gem list rails --remote --exact | head -1 (checks RubyGems, not just what's installed locally). As of this writing: Rails 8.1.3.~/.rbenv/versions/ (ls ~/.rbenv/versions | sort -V | tail). As of this writing: Ruby 4.0.1. Set it in the app's .ruby-version. Ruby 4.0.1 works with Rails 8.1 + RES 3.0 + the infra/domain gems.~/.rbenv/versions/{ruby}/bin/rails -v.Invoke rails/bundle via the full rbenv path for the chosen Ruby, e.g. ~/.rbenv/versions/4.0.1/bin/rails, ~/.rbenv/versions/4.0.1/bin/bundle — the rails shell alias may resolve to a different Ruby.
Commit frequently at natural checkpoints, not once at the end. Commit directly on the current branch — do not create a feature branch. Suggested commits: (1) after generating the app + configuring the Gemfile, (2) after RES wiring is green, (3) after each domain/read model. Per project convention, use the /commit skill and never mention Claude in commit messages.
Before writing any code, clarify:
crm, inventory_tracker)domains/ or new ones to be created)Run from the apps/ directory, using the full rbenv path for the newest Ruby (see Version policy above):
cd apps && ~/.rbenv/versions/{ruby}/bin/rails new {app_name} --database=postgresql --css=tailwind --skip-test
--skip-test (not --skip-test-unit, which no longer exists) drops the default test setup so we can install our own minitest + mutant harness.
Important post-generation steps:
echo "{ruby}" > apps/{app_name}/.ruby-version (the generator pins the global default, which may not be the newest).rails new creates a nested .git directory inside the new app. It must be removed so the app is part of the parent repo. rm -rf is blocked by the repo's git-safety hook, so ask the user to run it manually: rm -rf apps/{app_name}/.git. Do not proceed to committing until it's gone.Add these gems to the generated Gemfile (after jbuilder, before the tzinfo/solid gems). Use RES 3.0 to match rails_application:
gem "rails_event_store", ">= 3.0", "< 4.0"
gem "arkency-command_bus"
gem "infra", path: "../../infra"
Rails 8.1 with --skip-test does not generate a capybara/selenium test group. Just add a test group:
group :test do
gem "mutant-minitest"
end
Run bundle install (via the full rbenv path). Commit the generated app + Gemfile now (checkpoint 1).
Create config/initializers/rails_event_store.rb:
require "rails_event_store"
require "arkency/command_bus"
require_relative "../../lib/configuration"
Rails.configuration.to_prepare do
Rails.configuration.event_store = Infra::EventStore.main
Rails.configuration.command_bus = Arkency::CommandBus.new
Configuration.new.call(Rails.configuration.event_store, Rails.configuration.command_bus)
end
Create lib/configuration.rb. For the initial scaffold there is no domain yet, so call only wires event linking — this keeps the app bootable and tests green before any domain exists:
require_relative "../../../infra/lib/infra"
class Configuration
def call(event_store, command_bus)
enable_res_infra_event_linking(event_store)
end
private
def enable_res_infra_event_linking(event_store)
[
RailsEventStore::LinkByEventType.new,
RailsEventStore::LinkByCorrelationId.new,
RailsEventStore::LinkByCausationId.new
].each { |h| event_store.subscribe_to_all_events(h) }
end
end
As domains and read models are added later, extend call: require_relative the domain, add {DomainModule}::Configuration.new.call(event_store, command_bus), and give each read model its own enable_* private method called from call. Do not add a require_relative to a domain that doesn't exist yet — it will break boot.
In RES 3.0 the generator was renamed — the old rails_event_store_active_record:migration no longer exists. Use the ruby_event_store namespace with a data type (jsonb pairs cleanly with infra's RailsEventStore::JSONClient):
cd apps/{app_name} && ~/.rbenv/versions/{ruby}/bin/bundle exec rails generate ruby_event_store:active_record:migration --data-type=jsonb
Then rails db:create && rails db:migrate (via the full rbenv path). The generated migration creates event_store_events and event_store_events_in_streams with uuid event_id and jsonb data/metadata.
Ensure app/controllers/application_controller.rb has:
class ApplicationController < ActionController::Base
allow_browser versions: :modern
def command_bus
Rails.configuration.command_bus
end
def event_store
Rails.configuration.event_store
end
end
Replace the generated test/test_helper.rb (which has parallelize and fixtures :all):
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require "mutant/minitest/coverage"
ActiveJob::Base.logger = Logger.new(nil)
class InMemoryRESTestCase < ActiveSupport::TestCase
def before_setup
result = super
@previous_event_store = Rails.configuration.event_store
@previous_command_bus = Rails.configuration.command_bus
Rails.configuration.event_store = Infra::EventStore.in_memory
Rails.configuration.command_bus = Arkency::CommandBus.new
Configuration.new.call(
Rails.configuration.event_store, Rails.configuration.command_bus
)
result
end
def before_teardown
result = super
Rails.configuration.event_store = @previous_event_store
Rails.configuration.command_bus = @previous_command_bus
result
end
def event_store
Rails.configuration.event_store
end
def command_bus
Rails.configuration.command_bus
end
end
class InMemoryRESIntegrationTestCase < ActionDispatch::IntegrationTest
def before_setup
result = super
@previous_event_store = Rails.configuration.event_store
@previous_command_bus = Rails.configuration.command_bus
Rails.configuration.event_store = Infra::EventStore.in_memory_rails
Rails.configuration.command_bus = Arkency::CommandBus.new
Configuration.new.call(Rails.configuration.event_store, Rails.configuration.command_bus)
result
end
def before_teardown
result = super
Rails.configuration.event_store = @previous_event_store
Rails.configuration.command_bus = @previous_command_bus
result
end
def command_bus
Rails.configuration.command_bus
end
end
Create .mutant.yml in the app root. On the initial scaffold there are no subjects yet, so start with empty lists (mutant isn't run until there's domain code to cover):
includes:
- test
requires:
- ./config/environment
integration: minitest
usage: opensource
coverage_criteria:
timeout: true
process_abort: true
matcher:
subjects: []
ignore: []
As each read model is added, append its namespace to subjects (e.g. - Tweets*) and its AR model + Configuration#call to ignore. See apps/crm/.mutant.yml for a fully-populated example.
Add targets to the root Makefile:
install-{app_name}:
@make -C apps/{app_name} install
test-{app_name}:
@make -C apps/{app_name} test
mutate-{app_name}:
@make -C apps/{app_name} mutate
Add install-{app_name} to the install: target, test-{app_name} to test:, and mutate-{app_name} to mutate:.
Create Makefile in the app directory:
install:
@bin/setup
dev:
@make -j 2 web css
test:
@bin/rails test
mutate:
@RAILS_ENV=test bundle exec mutant run
tailwind:
@bin/rails tailwindcss:build
css:
@bin/rails tailwindcss:watch
web:
@bin/rails server -p {port}
.PHONY: install dev test mutate tailwind css web
Use a unique port per app (3000 for rails_application, 3001+ for new apps).
Controllers dispatch commands and query read models:
class ResourceController < ApplicationController
def index
@records = ReadModelName.all
end
def create
id = SecureRandom.uuid
ActiveRecord::Base.transaction do
command_bus.call(DomainModule::CreateCommand.new(id: id, ...))
end
redirect_to root_path
end
def update
command_bus.call(DomainModule::UpdateCommand.new(id: params[:id], ...))
redirect_to root_path
end
def destroy
command_bus.call(DomainModule::DeleteCommand.new(id: params[:id]))
redirect_to root_path
end
end
Build the app incrementally, with tests passing at each step:
/domain skill)/read-model skill)make test and mutation testingrails/bundle alias: The shell alias may resolve to the wrong Ruby. Always use the full rbenv path: ~/.rbenv/versions/{ruby}/bin/rails, ~/.rbenv/versions/{ruby}/bin/bundle..git: rails new creates its own git repo. It must be removed before the app can be committed to the parent repo. rm -rf is blocked by the git-safety hook — ask the user to run rm -rf apps/{app_name}/.git manually..ruby-version: The generator pins the rbenv global default, not necessarily the newest — overwrite it with the chosen Ruby.ruby_event_store:active_record:migration, not the old rails_event_store_active_record:migration (see step 6).bin/rails commands (migrations, generators, tests): Must run from apps/{app_name}/ directory, not project root.apps/require_relative "../../../domains/{name}/lib/{name}"gem "infra", path: "../../infra"app/read_models/{name}/configuration.rbevent.data.fetch(:key)command_bus.call(...) for writes, read models for queriesSecureRandom.uuid)