# New App

> Scaffold a new Rails app with event sourcing, following project conventions (todomvc as reference) Use when this capability is needed.

- Skill: `tomevault-io/new-app` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/new-app`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/new-app/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/new-app

---


# New App Scaffold

## When to use

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.

## Reference

The `apps/todo_mvc` app is the canonical reference. All patterns below are extracted from it.

## Step-by-step process

### 1. Gather requirements

Before writing any code, clarify:
- The **app name** (snake_case, e.g. `crm`, `inventory_tracker`)
- Which **domain modules** it will use (existing ones from `domains/` or new ones to be created)
- What the app **does** at a high level — what entities, what user actions

### 2. Generate the Rails app

Run from the `apps/` directory. The `rails` alias may not work — use the full rbenv path:

```bash
cd apps && /Users/andrzej/.rbenv/versions/3.4.6/bin/rails new {app_name} --database=postgresql --css=tailwind --skip-test-unit
```

**Important post-generation steps:**
- `rails new` creates a nested `.git` directory inside the new app. **Remove it** so the app is part of the parent repo: ask the user to run `rm -rf apps/{app_name}/.git`
- The generated test group includes `capybara` and `selenium-webdriver` — **replace** the entire test group with `mutant-minitest` (see step 3)

### 3. Configure Gemfile

Add these gems to the generated Gemfile (after the last gem before the comments):

```ruby
gem "rails_event_store", ">= 2.15.0", "< 3.0"
gem "arkency-command_bus"
gem "infra", path: "../../infra"
```

**Replace** the generated test group (which has capybara/selenium) with:

```ruby
group :test do
  gem "mutant-minitest"
end
```

Run `bundle install`.

### 4. Create event store initializer

Create `config/initializers/rails_event_store.rb`:

```ruby
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
```

### 5. Create app-level Configuration

Create `lib/configuration.rb`:

```ruby
require_relative "../../../domains/{domain_name}/lib/{domain_name}"
require_relative "../../../infra/lib/infra"

class Configuration
  def call(event_store, command_bus)
    enable_res_infra_event_linking(event_store)
    # enable_{read_model_name}_read_model(event_store) — add as read models are created

    {DomainModule}::Configuration.new.call(event_store, command_bus)
  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
```

Each read model gets its own `enable_*` private method, called from `call`.

### 6. Create event store migration

Generate the RES migration:

```bash
cd apps/{app_name} && rails generate rails_event_store_active_record:migration
```

Run `rails db:create && rails db:migrate`.

### 7. Create ApplicationController

Ensure `app/controllers/application_controller.rb` has:

```ruby
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
```

### 8. Create test helper

**Replace** the generated `test/test_helper.rb` (which has `parallelize` and `fixtures :all`):

```ruby
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
```

### 9. Create .mutant.yml

Create `.mutant.yml` in the app root:

```yaml
includes:
  - test
requires:
  - ./config/environment
integration: minitest
usage: opensource
coverage_criteria:
  timeout: true
  process_abort: true
matcher:
  subjects:
    - ReadModelName*
  ignore:
    - ReadModelName::ModelClass
    - ReadModelName::Configuration#call
```

Add each read model's namespace to `subjects` and its AR model + Configuration#call to `ignore`.

### 10. Register in root Makefile

Add targets to the root `Makefile`:

```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:`.

### 11. Create app Makefile

Create `Makefile` in the app directory:

```makefile
install:
	@bin/setup

dev:
	@make -j 2 web css

test:
	@bin/rails test

mutate:
	@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).

## Controller pattern

Controllers dispatch commands and query read models:

```ruby
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 order

Build the app incrementally, with tests passing at each step:

1. Scaffold Rails app + boilerplate (steps 2-11)
2. Create or reference domain modules (use the `/domain` skill)
3. Add read models one at a time (use the `/read-model` skill)
4. Add controllers + views for each read model
5. Add integration tests
6. Run `make test` and mutation testing

## Gotchas

- **`rails` command**: The shell alias may not resolve. Use full path: `/Users/andrzej/.rbenv/versions/3.4.6/bin/rails new`
- **Nested `.git`**: `rails new` creates its own git repo. Must be removed before committing to parent repo
- **`bin/rails` commands** (migrations, generators): Must run from `apps/{app_name}/` directory, not project root
- **Generated boilerplate**: Rails generates fixtures, system tests, channels etc. These are harmless but unused — leave them or ask user to clean up

## Key conventions

- Each app is a standalone Rails app in `apps/`
- Apps reference domains via `require_relative "../../../domains/{name}/lib/{name}"`
- Apps reference infra via `gem "infra", path: "../../infra"`
- Read models live in `app/read_models/{name}/configuration.rb`
- All event access uses `event.data.fetch(:key)`
- Controllers use `command_bus.call(...)` for writes, read models for queries
- UUIDs for all business entity IDs (`SecureRandom.uuid`)
- Test-first TDD, 100% mutation score

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/railseventstore) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-11 -->

