command_tower 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -8
  3. data/app/controllers/command_tower/me/inbox_controller.rb +16 -0
  4. data/app/deserializers/command_tower/deserializers/messaging/inbox.rb +85 -0
  5. data/app/jobs/command_tower/messaging/communications/produce_recipient_job.rb +2 -0
  6. data/app/models/command_tower/messaging/communication.rb +31 -0
  7. data/app/serializers/command_tower/serializers/messaging/inbox.rb +43 -0
  8. data/app/services/command_tower/messaging/accept/coordinator.rb +16 -1
  9. data/app/services/command_tower/messaging/accept/persister.rb +13 -0
  10. data/app/services/command_tower/messaging/contract/mappers/communication_mapper.rb +7 -0
  11. data/app/services/command_tower/messaging/contract/results/communication_result.rb +1 -0
  12. data/app/services/command_tower/messaging/inbox/conversation_result.rb +31 -0
  13. data/app/services/command_tower/messaging/inbox/entry_result.rb +36 -0
  14. data/app/services/command_tower/messaging/inbox/reader.rb +231 -18
  15. data/app/services/command_tower/messaging/inbox.rb +4 -0
  16. data/app/services/command_tower/messaging/rendering/inbox_document_renderer.rb +53 -36
  17. data/app/services/command_tower/messaging/rendering/inbox_presentation_resolver.rb +52 -0
  18. data/app/services/command_tower/messaging/rendering/inbox_presentation_snapshot.rb +64 -0
  19. data/app/services/command_tower/messaging.rb +5 -1
  20. data/app/services/command_tower/services/messaging/communications/produce.rb +4 -0
  21. data/app/services/command_tower/services/messaging/communications/produce_many.rb +6 -0
  22. data/app/services/command_tower/services/messaging/inbox.rb +47 -1
  23. data/app/workflows/command_tower/workflows/messaging/communications/produce_recipient_workflow.rb +5 -1
  24. data/app/workflows/command_tower/workflows/messaging/inbox.rb +15 -1
  25. data/config/routes.rb +1 -0
  26. data/db/migrate/20261010000001_add_inbox_presentation_snapshot_to_messaging_communications.rb +7 -0
  27. data/db/migrate/20261010000002_add_conversation_identity_to_messaging_communications.rb +10 -0
  28. data/docs/authorization.md +1 -1
  29. data/docs/bootstrap/00-ownership.md +77 -0
  30. data/docs/bootstrap/01-docker-make-compose.md +267 -0
  31. data/docs/bootstrap/02-create-the-rails-app.md +75 -0
  32. data/docs/bootstrap/03-pin-the-gem.md +53 -0
  33. data/docs/bootstrap/04-secrets-and-env.md +59 -0
  34. data/docs/bootstrap/05-install-migrate-doctor.md +74 -0
  35. data/docs/bootstrap/06-mount-and-health.md +53 -0
  36. data/docs/bootstrap/07-execution-bases.md +38 -0
  37. data/docs/bootstrap/08-initializer.md +90 -0
  38. data/docs/bootstrap/09-rbac.md +83 -0
  39. data/docs/bootstrap/10-roles-and-gates.md +62 -0
  40. data/docs/bootstrap/11-auth-client-path.md +57 -0
  41. data/docs/bootstrap/12-smoke-check.md +81 -0
  42. data/docs/bootstrap/13-optional.md +50 -0
  43. data/docs/bootstrap/14-sanity-checks.md +44 -0
  44. data/docs/bootstrap/README.md +90 -0
  45. data/docs/cookie_authentication_guide.md +11 -0
  46. data/docs/extending.md +3 -3
  47. data/docs/host_integration_guide.md +3 -281
  48. data/docs/initializing.md +37 -28
  49. data/docs/messaging_integration_guide.md +1 -1
  50. data/docs/principal_capabilities.md +1 -1
  51. data/docs/upgrades/0.10.0.md +5 -5
  52. data/docs/upgrades/0.11.0.md +5 -5
  53. data/docs/upgrades/0.18.0.md +35 -0
  54. data/docs/upgrades/README.md +2 -1
  55. data/lib/command_tower/authorization/default.yml +1 -0
  56. data/lib/command_tower/install/baseline.rb +2 -0
  57. data/lib/command_tower/version.rb +1 -1
  58. metadata +25 -2
@@ -26,12 +26,26 @@ module CommandTower
26
26
  result = CommandTower::Services::Messaging::Inbox::List.call(user:, limit:, offset:, scope:)
27
27
  return result_or_failure(result) unless result.success?
28
28
 
29
- payload = result.data[:items].map { |item| CommandTower::Serializers::Messaging::Inbox::ItemSerializer.serialize(item) }
29
+ payload = result.data[:items].map { |item| CommandTower::Serializers::Messaging::Inbox::EntrySerializer.serialize(item) }
30
30
  meta = CommandTower::Serializers::Messaging::Inbox::PaginationMetaSerializer.serialize(result.data[:pagination])
31
31
  success(payload:, meta:, http_status: :ok)
32
32
  end
33
33
  end
34
34
 
35
+ class ConversationWorkflow < BaseWorkflow
36
+ def call(user:, key:, scope:, around: nil, before_id: nil, after_id: nil, limit: nil)
37
+ result = CommandTower::Services::Messaging::Inbox::Conversation.call(
38
+ user:, key:, scope:, around:, before_id:, after_id:, limit:,
39
+ )
40
+ return result_or_failure(result) unless result.success?
41
+
42
+ success(
43
+ payload: CommandTower::Serializers::Messaging::Inbox::ConversationSerializer.serialize(result.data[:conversation]),
44
+ http_status: :ok,
45
+ )
46
+ end
47
+ end
48
+
35
49
  class ShowWorkflow < BaseWorkflow
36
50
  def call(user:, inbox_item_id:)
37
51
  result = CommandTower::Services::Messaging::Inbox::Show.call(user:, inbox_item_id:)
data/config/routes.rb CHANGED
@@ -64,6 +64,7 @@ CommandTower::Engine.routes.draw do
64
64
 
65
65
  resources :inbox, only: [:index, :show, :destroy], controller: "inbox" do
66
66
  collection do
67
+ get :conversation
67
68
  get :unread_count, path: "unread-count"
68
69
  post "bulk/read", action: :bulk_read
69
70
  post "bulk/unread", action: :bulk_unread
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddInboxPresentationSnapshotToMessagingCommunications < ActiveRecord::Migration[7.2]
4
+ def change
5
+ add_column :messaging_communications, :inbox_presentation_snapshot, :json
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddConversationIdentityToMessagingCommunications < ActiveRecord::Migration[7.2]
4
+ def change
5
+ add_column :messaging_communications, :conversation_key, :string, limit: 128
6
+ add_column :messaging_communications, :conversation_title, :string
7
+ add_index :messaging_communications, %i[user_id conversation_key],
8
+ name: "index_messaging_communications_on_user_and_conversation_key"
9
+ end
10
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  Authorization establishes **permission** after authentication. Failed authorization returns `403`.
4
4
 
5
- Host `rbac_groups.yml` is a **required integration step** (Step 4) — see [Host integration](host_integration_guide.md#step-4--host-rbac-required). CommandTower ships CT-owned entity definitions. Hosts grant those names to product roles; they must not copy CT controller mappings. Without a host role that grants Me/Auth entities, authenticated calls return **403**.
5
+ Host `rbac_groups.yml` is a **required integration step** — see [Bootstrap — RBAC](bootstrap/09-rbac.md). CommandTower ships CT-owned entity definitions. Hosts grant those names to product roles; they must not copy CT controller mappings. Without a host role that grants Me/Auth entities, authenticated calls return **403**.
6
6
 
7
7
  ## Quick usage (host provisional)
8
8
 
@@ -0,0 +1,77 @@
1
+ # 00 — Ownership
2
+
3
+ ## Purpose
4
+
5
+ Know what CommandTower owns vs what the host owns **before** you generate a Rails app. Wrong ownership here produces forked platform code and duplicate schema.
6
+
7
+ ## Prerequisites
8
+
9
+ Read [`README.md`](README.md). This step is conceptual; no files yet.
10
+
11
+ ## Engine vs host
12
+
13
+ CommandTower is a **mountable Rails engine**. The host is a Rails API application that:
14
+
15
+ - Depends on the `command_tower` gem
16
+ - Runs install/migrate/doctor **inside Compose**
17
+ - Mounts the engine
18
+ - Configures secrets, RBAC composition, and feature gates
19
+ - Adds **product** routes, workflows, and schema
20
+
21
+ The engine owns:
22
+
23
+ - CommandTower-owned schema (users, sessions, JWT-related tables, Me/Auth, messaging platform tables, and the rest of the engine `db/migrate`)
24
+ - Platform HTTP under the mount
25
+ - Workflow / service / serializer framework
26
+ - Default entity catalog (`lib/command_tower/authorization/default.yml`)
27
+ - Shared FactoryBot definitions
28
+
29
+ The host owns:
30
+
31
+ - `Dockerfile`, `docker-compose.yml`, `Makefile`
32
+ - Product schema, product routes, product workflows
33
+ - `config/initializers/command_tower.rb` after generate
34
+ - `config/rbac_groups.yml` (grants of **CT entity names** plus host-owned entities)
35
+ - Secrets in Compose / env
36
+ - Feature-gate choices
37
+ - Messaging catalogs and adapter credentials (optional)
38
+
39
+ ## Schema (AD-SCH-01)
40
+
41
+ CommandTower is the **sole authoring authority** for CommandTower-owned schema. Hosts must **not** manually write, edit, or copy-paste CommandTower schema migrations.
42
+
43
+ Hosts install copies via `command_tower:install` / `command_tower:install:migrations`, then `db:migrate`. Installed `*.command_tower.rb` files are an **execution context**, not a second authoring home.
44
+
45
+ Contract: [`../initializing.md`](../initializing.md).
46
+
47
+ ## Workflow layer
48
+
49
+ Meaningful business behavior enters through a **workflow**. Controllers, jobs, and scheduled tasks do not call services as a full business action. Workflows do not call other workflows (extract a shared sequence). See [`../architecture.md`](../architecture.md).
50
+
51
+ Do not invent a parallel “manager” layer.
52
+
53
+ ## Dummy `rails_app` is not a product template
54
+
55
+ [`../../rails_app/`](../../rails_app/) is the **in-engine dummy host** used to develop and test the gem. Later steps cite its initializer and `rbac_groups.yml` for **shape**.
56
+
57
+ Do not copy `rails_app/` as a new product. Do not copy this gem’s [`docker-compose.yaml`](../../docker-compose.yaml) (`mysql:latest`, engine ports) — that file is for **engine development**, not a product host.
58
+
59
+ This tree writes **generic** Makefile / Compose templates in [`01-docker-make-compose.md`](01-docker-make-compose.md).
60
+
61
+ ## Docker-only
62
+
63
+ No Ruby or Rails on the host OS. See the README **Docker-only** section. Step 01 exists so `rails new` never runs on the Mac.
64
+
65
+ ## What a platform-proof host is
66
+
67
+ Enough to:
68
+
69
+ 1. Boot in Compose
70
+ 2. Pass `make doctor`
71
+ 3. Register, log in, and receive **200** from `GET /me` as a `member`
72
+
73
+ Product domain, Solid Queue workers, SMS/Pushover adapters, and admin RBAC bundles are **not** required to finish this manual.
74
+
75
+ ## Stop
76
+
77
+ You can explain engine vs host, AD-SCH-01, and why Docker exists before `rails new`. Next: [`01-docker-make-compose.md`](01-docker-make-compose.md).
@@ -0,0 +1,267 @@
1
+ # 01 — Docker, Make, Compose
2
+
3
+ ## Purpose
4
+
5
+ Author the local toolchain **before** any Rails app exists. After this step you can `make help` and `make build` on an empty repository. Ruby is only inside the image.
6
+
7
+ ## Prerequisites
8
+
9
+ - Docker Desktop (or equivalent) with the Compose plugin
10
+ - Make
11
+ - Git
12
+ - Empty (or nearly empty) host repository
13
+
14
+ Do **not** install Ruby, Bundler, or Rails on the Mac.
15
+
16
+ ## Files the host creates
17
+
18
+ | File | Role |
19
+ |------|------|
20
+ | `Dockerfile` | Ruby image: Bundler, MariaDB client, Node (asset/JS tooling if needed) |
21
+ | `docker-compose.yml` | `api`, MariaDB **10.11+**, Redis |
22
+ | `Makefile` | Operator surface: `build`, `rails-new`, `rails`, `migrate`, `doctor`, … |
23
+ | `.dockerignore` | Keep the build context small |
24
+ | `.env` (gitignored) | Local secrets later ([`04-secrets-and-env.md`](04-secrets-and-env.md)); create a stub if Compose interpolates vars |
25
+
26
+ The database in **this recipe** is MariaDB 10.11+ with `utf8mb4` / `utf8mb4_unicode_ci`. That is a host choice documented here. Do not copy the gem’s engine-dev compose (`mysql:latest`).
27
+
28
+ Redis is required for CommandTower cache/features that expect it. Optional `worker` is only if the host uses Solid Queue (add after the app exists).
29
+
30
+ ## Procedure
31
+
32
+ ### 1. `.dockerignore`
33
+
34
+ ```
35
+ .git
36
+ log
37
+ tmp
38
+ .env
39
+ coverage
40
+ vendor/bundle
41
+ node_modules
42
+ .DS_Store
43
+ ```
44
+
45
+ ### 2. `Dockerfile`
46
+
47
+ Use an MRI image CommandTower’s doctor accepts (Rails **>= 7 and < 9**). A current floor that matches engine CI is Ruby **4.0.x** and Rails **8.1**. Do not freeze an older patch than the gem you will pin in step 03.
48
+
49
+ The image must **bind Puma on `0.0.0.0`**. Binding `127.0.0.1` inside the container is not reachable from the Mac.
50
+
51
+ ```dockerfile
52
+ FROM ruby:4.0.1
53
+
54
+ ENV RAILS_LOG_TO_STDOUT=1
55
+ ENV BUNDLE_PATH=/usr/local/bundle
56
+ ENV BUNDLE_JOBS=4
57
+
58
+ RUN apt-get update -qq && apt-get install --no-install-recommends -y \
59
+ build-essential \
60
+ git \
61
+ default-libmysqlclient-dev \
62
+ default-mysql-client \
63
+ libyaml-dev \
64
+ pkg-config \
65
+ redis-tools \
66
+ && rm -rf /var/lib/apt/lists/*
67
+
68
+ WORKDIR /app
69
+
70
+ # Gemfile does not exist until `make rails-new`. Copy the context; install gems when present.
71
+ COPY . .
72
+ RUN if [ -f Gemfile ]; then bundle install; fi
73
+
74
+ EXPOSE 3000
75
+ CMD ["bundle", "exec", "bin/rails", "server", "-b", "0.0.0.0", "-p", "3000"]
76
+ ```
77
+
78
+ Until [`02-create-the-rails-app.md`](02-create-the-rails-app.md) runs, there is **no** `Gemfile`. That is expected. `make build` still produces an image with Ruby, Bundler, and the MariaDB client. `make rails-new` uses that image.
79
+
80
+ After `rails new` you will have a Gemfile; rebuild so `bundle install` during image build caches gems. Day-to-day `make bundle` still refreshes gems in the running volume.
81
+
82
+ ### 3. `docker-compose.yml`
83
+
84
+ ```yaml
85
+ services:
86
+ api:
87
+ build: .
88
+ working_dir: /app
89
+ command: bundle exec bin/rails server -b 0.0.0.0 -p 3000
90
+ volumes:
91
+ - .:/app
92
+ - bundle_cache:/usr/local/bundle
93
+ ports:
94
+ - "3000:3000"
95
+ environment:
96
+ RAILS_ENV: development
97
+ DATABASE_URL: mysql2://app:app@db:3306/app_development
98
+ REDIS_URL: redis://redis:6379/0
99
+ SECRET_KEY_BASE: ${SECRET_KEY_BASE:-dev-secret-key-base-change-me}
100
+ SIGNUP_SESSION_JWT_SECRET: ${SIGNUP_SESSION_JWT_SECRET:-dev-signup-session-change-me}
101
+ PASSWORD_RECOVERY_SESSION_JWT_SECRET: ${PASSWORD_RECOVERY_SESSION_JWT_SECRET:-dev-password-recovery-change-me}
102
+ CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:8081,http://localhost:8082,http://localhost:19006}
103
+ depends_on:
104
+ db:
105
+ condition: service_healthy
106
+ redis:
107
+ condition: service_started
108
+ stdin_open: true
109
+ tty: true
110
+
111
+ db:
112
+ image: mariadb:10.11
113
+ environment:
114
+ MARIADB_ROOT_PASSWORD: root
115
+ MARIADB_DATABASE: app_development
116
+ MARIADB_USER: app
117
+ MARIADB_PASSWORD: app
118
+ ports:
119
+ - "3306:3306"
120
+ volumes:
121
+ - mariadb_data:/var/lib/mysql
122
+ command:
123
+ - --character-set-server=utf8mb4
124
+ - --collation-server=utf8mb4_unicode_ci
125
+ healthcheck:
126
+ test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
127
+ interval: 5s
128
+ timeout: 5s
129
+ retries: 20
130
+
131
+ redis:
132
+ image: redis:7-alpine
133
+ ports:
134
+ - "6379:6379"
135
+
136
+ volumes:
137
+ bundle_cache:
138
+ mariadb_data:
139
+ ```
140
+
141
+ Rename `app_development` / user / password to product names when you have them. Keep utf8mb4.
142
+
143
+ `command:` (and the Dockerfile `CMD`) must use `-b 0.0.0.0`. Without it, `make server` is not reachable from the Mac or from an Expo app on the host OS.
144
+
145
+ Published port **3000** is this recipe’s default for a single local API. The frontend `HostConfig` API origin must use the **same** host-published port. A second Command Tower API on this machine publishes a different host port. Engine-dev compose in this gem uses different ports; ignore it.
146
+
147
+ `CORS_ALLOWED_ORIGINS` in the sample (`8081`, `8082`, `19006`) is one web product’s origins. A second local web app uses its own Expo port. Change that origin, this API’s published port, and `config.application.url` (step 08) together. The list is consumed in [`11-auth-client-path.md`](11-auth-client-path.md). `curl` on the Mac does not need CORS.
148
+
149
+ ### 4. `Makefile`
150
+
151
+ Generic operator surface. `COMPOSE` is the only Compose invocation.
152
+
153
+ ```makefile
154
+ .PHONY: help build bundle setup fresh rebuild rails-new rails db-prepare migrate doctor \
155
+ bash console runner server s halt down down-v rspec test spec rubocop
156
+
157
+ COMPOSE := docker compose
158
+ API := $(COMPOSE) run --rm api
159
+ API_T := $(COMPOSE) run --rm -T api
160
+
161
+ help:
162
+ @echo "build Build the api image"
163
+ @echo "bundle bundle install in the container"
164
+ @echo "setup build + db-prepare"
165
+ @echo "fresh down-v + setup"
166
+ @echo "rebuild build --no-cache + bundle"
167
+ @echo "rails-new rails new (API) inside the container"
168
+ @echo "rails Forward to bin/rails (ARGS=...)"
169
+ @echo "db-prepare db:prepare"
170
+ @echo "migrate db:migrate"
171
+ @echo "doctor command_tower:doctor"
172
+ @echo "bash Shell in api"
173
+ @echo "console rails console"
174
+ @echo "runner rails runner CMD='...'"
175
+ @echo "server / s Foreground api server"
176
+ @echo "halt Stop compose"
177
+ @echo "down compose down"
178
+ @echo "down-v compose down -v (destroys DB volume)"
179
+ @echo "rspec/test rspec"
180
+ @echo "spec rspec SPEC=path"
181
+ @echo "rubocop rubocop"
182
+
183
+ build:
184
+ $(COMPOSE) build api
185
+
186
+ bundle:
187
+ $(API) bundle install
188
+
189
+ setup: build db-prepare
190
+
191
+ fresh: down-v setup
192
+
193
+ rebuild:
194
+ $(COMPOSE) build --no-cache api
195
+ $(MAKE) bundle
196
+
197
+ # Chicken-and-egg: image has Ruby; Gemfile may not exist yet.
198
+ # Doctor accepts Rails >= 7 and < 9. Prefer 8.1 to match current engine CI.
199
+ RAILS_NEW_VERSION ?= ~> 8.1.0
200
+ rails-new:
201
+ $(API) bash -lc 'gem install rails -v "$(RAILS_NEW_VERSION)" --no-document && rails new . --api --database=mysql --skip-git --force'
202
+
203
+ rails:
204
+ $(COMPOSE) run --rm -e SKIP_MOUNT -e SKIP_CONFIGURE -e FORCE api bundle exec bin/rails $(ARGS)
205
+
206
+ db-prepare:
207
+ $(API) bundle exec bin/rails db:prepare
208
+
209
+ migrate:
210
+ $(API) bundle exec bin/rails db:migrate
211
+
212
+ doctor:
213
+ $(API) bundle exec bin/rails command_tower:doctor
214
+
215
+ bash:
216
+ $(COMPOSE) run --rm api bash
217
+
218
+ console:
219
+ $(API) bundle exec bin/rails console
220
+
221
+ runner:
222
+ $(API) bundle exec bin/rails runner "$(CMD)"
223
+
224
+ server s:
225
+ $(COMPOSE) up api
226
+
227
+ halt:
228
+ $(COMPOSE) stop
229
+
230
+ down:
231
+ $(COMPOSE) down
232
+
233
+ down-v:
234
+ $(COMPOSE) down -v
235
+
236
+ rspec test:
237
+ $(API_T) bundle exec rspec
238
+
239
+ spec:
240
+ $(API_T) bundle exec rspec $(SPEC)
241
+
242
+ rubocop:
243
+ $(API_T) bundle exec rubocop
244
+ ```
245
+
246
+ `rails-new` installs the Rails **gem inside the ephemeral container** and writes the app onto the mounted `/app`. That is still Docker-only: the Mac never runs `gem` or `rails`.
247
+
248
+ After the app exists, prefer `make rails ARGS='…'` over re-running `rails-new`.
249
+
250
+ Flags such as `SKIP_MOUNT=1` belong on the Make line. The `rails` recipe forwards `SKIP_MOUNT`, `SKIP_CONFIGURE`, and `FORCE` into the container:
251
+
252
+ ```bash
253
+ SKIP_MOUNT=1 make rails ARGS='command_tower:install'
254
+ ```
255
+
256
+ ### 5. Build
257
+
258
+ ```bash
259
+ make help
260
+ make build
261
+ ```
262
+
263
+ `make build` may warn that `Gemfile` is missing. That is expected until step 02.
264
+
265
+ ## Stop
266
+
267
+ `make help` prints the operator surface. `make build` produces an `api` image with Ruby. There is still no Rails application. Next: [`02-create-the-rails-app.md`](02-create-the-rails-app.md).
@@ -0,0 +1,75 @@
1
+ # 02 — Create the Rails app
2
+
3
+ ## Purpose
4
+
5
+ Generate a Rails **8.1 API** application **inside the Compose image**. The host OS never receives a `rails` binary.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`01-docker-make-compose.md`](01-docker-make-compose.md) complete: `make build` works
10
+ - Working directory is the host repository (already contains Dockerfile / Compose / Makefile)
11
+
12
+ ## Files this step creates (inside the container, onto the mount)
13
+
14
+ Rails’ `rails new .` writes `Gemfile`, `config/`, `app/`, `bin/`, and the rest of a standard API app into the current directory.
15
+
16
+ `--force` is required because Dockerfile, Compose, and Makefile already exist.
17
+
18
+ ## Procedure
19
+
20
+ ```bash
21
+ make rails-new
22
+ ```
23
+
24
+ That target (from step 01) runs in the `api` service:
25
+
26
+ ```text
27
+ gem install rails -v "~> 8.1.0" --no-document
28
+ rails new . --api --database=mysql --skip-git --force
29
+ ```
30
+
31
+ Both commands run **in the container**. `--skip-git` avoids nesting a repo; you already have Git on the host. `--database=mysql` matches MariaDB via the `mysql2` adapter.
32
+
33
+ If `rails new` overwrites `Dockerfile`, `docker-compose.yml`, or `Makefile`, **restore the files from step 01**. Rails generators are not the source of the host toolchain. Confirm `command:` / `CMD` still bind `-b 0.0.0.0`.
34
+
35
+ Then add host gems CommandTower does not replace. In `Gemfile` (keep `mysql2` and `puma` from `rails new`):
36
+
37
+ ```ruby
38
+ gem "rack-cors"
39
+ ```
40
+
41
+ `command_tower` (step 03) already depends on `redis`. You do not need a second Redis gem unless the host uses Redis for its own cache.
42
+
43
+ Set `config/database.yml` so development points at the `db` service, not `localhost` on the Mac:
44
+
45
+ ```yaml
46
+ development:
47
+ adapter: mysql2
48
+ encoding: utf8mb4
49
+ collation: utf8mb4_unicode_ci
50
+ url: <%= ENV.fetch("DATABASE_URL") %>
51
+ ```
52
+
53
+ Leave test until you add a test database; `db:prepare` can create it later.
54
+
55
+ Local mail for verify/reset codes (Compose-mounted `tmp/` is visible on the Mac):
56
+
57
+ ```ruby
58
+ # config/environments/development.rb
59
+ config.action_mailer.perform_deliveries = true
60
+ config.action_mailer.delivery_method = :file
61
+ config.action_mailer.file_settings = { location: Rails.root.join("tmp/mails") }
62
+ ```
63
+
64
+ Then:
65
+
66
+ ```bash
67
+ make build
68
+ make bundle
69
+ ```
70
+
71
+ Do not run `rails new` on the Mac. Do not `brew install ruby` to make this step “easier.”
72
+
73
+ ## Stop
74
+
75
+ `Gemfile` exists, `bin/rails` exists **in the repo** (executed only via `make rails`). Dockerfile/Compose from step 01 are still the operator toolchain. `make bundle` succeeds. Next: [`03-pin-the-gem.md`](03-pin-the-gem.md).
@@ -0,0 +1,53 @@
1
+ # 03 — Pin the CommandTower gem
2
+
3
+ ## Purpose
4
+
5
+ Depend on released CommandTower from **RubyGems** (or a git tag). Path pins are maintainer Local mode, not the committed default for a new host.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`02-create-the-rails-app.md`](02-create-the-rails-app.md): `make bundle` works
10
+ - You know the CommandTower version (or git tag) this host should adopt
11
+
12
+ ## Files the host edits
13
+
14
+ `Gemfile` — add:
15
+
16
+ ```ruby
17
+ gem "command_tower"
18
+ ```
19
+
20
+ Pin when you need a floor:
21
+
22
+ ```ruby
23
+ gem "command_tower", "~> X.Y"
24
+ ```
25
+
26
+ Git tag (only if your org does not publish the gem to the RubyGems source the host uses):
27
+
28
+ ```ruby
29
+ gem "command_tower", git: "https://github.com/<org>/command_tower.git", tag: "vX.Y.Z"
30
+ ```
31
+
32
+ **Path pins** (`path: "../command_tower"`) are for engine maintainers iterating locally. They are not the default a new product host commits. If you use a path pin in a workspace, do not document it as the production pin.
33
+
34
+ ## Procedure
35
+
36
+ 1. Add the gem line.
37
+ 2. Refresh lockfile **in the container**:
38
+
39
+ ```bash
40
+ make bundle
41
+ ```
42
+
43
+ 3. Rebuild if the image copies `Gemfile` / `Gemfile.lock` at build time:
44
+
45
+ ```bash
46
+ make build
47
+ ```
48
+
49
+ Do not run `bundle install` on the Mac.
50
+
51
+ ## Stop
52
+
53
+ `Gemfile.lock` lists `command_tower`. `make bundle` succeeds. Next: [`04-secrets-and-env.md`](04-secrets-and-env.md).
@@ -0,0 +1,59 @@
1
+ # 04 — Secrets and env
2
+
3
+ ## Purpose
4
+
5
+ Give Compose the secrets CommandTower **doctor** requires. Without them, install may succeed and doctor will fail.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`03-pin-the-gem.md`](03-pin-the-gem.md)
10
+ - Compose file from [`01-docker-make-compose.md`](01-docker-make-compose.md)
11
+
12
+ ## Files the host creates / edits
13
+
14
+ | File | Role |
15
+ |------|------|
16
+ | `.env` (gitignored) | Local values interpolated by Compose |
17
+ | `.env.example` (committed, no secrets) | Names only |
18
+ | `docker-compose.yml` `environment:` | Already stubbed in step 01 |
19
+
20
+ Required (names match [`../initializing.md`](../initializing.md)):
21
+
22
+ | Secret | Typical env | Used for |
23
+ |--------|-------------|----------|
24
+ | JWT HMAC | `SECRET_KEY_BASE` (or `config.jwt.hmac_secret`) | Access tokens |
25
+ | Signup-session JWT | `SIGNUP_SESSION_JWT_SECRET` | Email verification / signup session |
26
+ | Password-recovery JWT | `PASSWORD_RECOVERY_SESSION_JWT_SECRET` | Password reset session |
27
+
28
+ Doctor checks these. Failures abort with remediation text.
29
+
30
+ `.env.example`:
31
+
32
+ ```
33
+ SECRET_KEY_BASE=
34
+ SIGNUP_SESSION_JWT_SECRET=
35
+ PASSWORD_RECOVERY_SESSION_JWT_SECRET=
36
+ ```
37
+
38
+ Generate long random values for `.env`. Do not commit `.env`. Do not reuse production secrets in development.
39
+
40
+ The initializer generated in step 05 can read these via `ENV.fetch`. You do not need a complete initializer yet.
41
+
42
+ Optional later: SMTP, SMS, Pushover credentials — [`13-optional.md`](13-optional.md) and [`../messaging_integration_guide.md`](../messaging_integration_guide.md). Not required for `GET /me`.
43
+
44
+ This step is **local** env for Compose. Production secret distribution is out of scope ([README](README.md#this-is-not-deployment)).
45
+
46
+ ## Procedure
47
+
48
+ 1. Create `.env` with three distinct secrets.
49
+ 2. Confirm `docker-compose.yml` passes them into `api`.
50
+ 3. Recreate the api container if it was already running:
51
+
52
+ ```bash
53
+ make halt
54
+ make build
55
+ ```
56
+
57
+ ## Stop
58
+
59
+ Compose interpolates the three secrets into `api`. Next: [`05-install-migrate-doctor.md`](05-install-migrate-doctor.md).
@@ -0,0 +1,74 @@
1
+ # 05 — Install, migrate, doctor
2
+
3
+ ## Purpose
4
+
5
+ Copy engine migrations, generate the initializer (and default mount unless skipped), migrate, and run doctor. **Invocation is Make.** The contract for flags and schema is [`../initializing.md`](../initializing.md) — do not treat this page as a second copy of that contract.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`04-secrets-and-env.md`](04-secrets-and-env.md)
10
+ - Database service healthy (`make` recipes that run Rails will start `depends_on`)
11
+
12
+ ## Files this step creates
13
+
14
+ | File | Role |
15
+ |------|------|
16
+ | `db/migrate/*command_tower.rb` | Installed copies of engine migrations (do not hand-edit) |
17
+ | `config/initializers/command_tower.rb` | Generated unless `SKIP_CONFIGURE=1` |
18
+ | Engine mount in `config/routes.rb` | Unless `SKIP_MOUNT=1` or `--skip-routes` |
19
+
20
+ `command_tower:install` does **not** run `db:migrate`. Installing and migrating stay separate on purpose.
21
+
22
+ ## Procedure
23
+
24
+ If the host will own `GET /api/healthz` (or any route that must be declared **before** the engine mount), skip the generator mount and add routes in [`06-mount-and-health.md`](06-mount-and-health.md):
25
+
26
+ ```bash
27
+ make rails ARGS='command_tower:install' SKIP_MOUNT=1
28
+ ```
29
+
30
+ Otherwise:
31
+
32
+ ```bash
33
+ make rails ARGS='command_tower:install'
34
+ ```
35
+
36
+ Then:
37
+
38
+ ```bash
39
+ make db-prepare
40
+ make migrate
41
+ make doctor
42
+ ```
43
+
44
+ `db-prepare` creates the development database if needed. `migrate` applies host and CommandTower migrations.
45
+
46
+ Existing customized hosts (initializer and mount already present):
47
+
48
+ ```bash
49
+ make rails ARGS='command_tower:install' SKIP_CONFIGURE=1
50
+ make migrate
51
+ make doctor
52
+ ```
53
+
54
+ `FORCE=1` overwrites `config/initializers/command_tower.rb` — do not use it after you have customized the initializer unless you intend to regenerate.
55
+
56
+ The **task name** inside the container is `bin/rails command_tower:install`. Operators never type that on the Mac.
57
+
58
+ ### What install does (summary)
59
+
60
+ 1. `command_tower:install:migrations` — copies engine migrations with `*.command_tower.rb` scope.
61
+ 2. `rails g command_tower:configure` unless skipped — initializer + mount.
62
+ 3. Prints next steps.
63
+
64
+ Full flag table: [`../initializing.md`](../initializing.md).
65
+
66
+ Doctor checks Rails compatibility, engine baseline migrations, host-installed copies, JWT / session secrets, and messaging adapter names. It does **not** probe Redis/SMTP connectivity.
67
+
68
+ ## In-engine dummy
69
+
70
+ Generated initializer shape: [`../../rails_app/config/initializers/command_tower.rb`](../../rails_app/config/initializers/command_tower.rb). That file is a dummy-host example, not a product dump.
71
+
72
+ ## Stop
73
+
74
+ Install is idempotent if re-run. Migrations applied. `make doctor` exits 0 (or you have a documented doctor failure you will fix in steps 08–10). Next: [`06-mount-and-health.md`](06-mount-and-health.md).