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
@@ -0,0 +1,53 @@
1
+ # 06 — Mount and health
2
+
3
+ ## Purpose
4
+
5
+ Mount CommandTower where the product needs it, and keep a host health endpoint **outside** the engine when you skipped the generator mount.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`05-install-migrate-doctor.md`](05-install-migrate-doctor.md)
10
+
11
+ ## Files the host edits
12
+
13
+ `config/routes.rb`
14
+
15
+ ## Procedure
16
+
17
+ ### Default (generator mounted)
18
+
19
+ `command_tower:install` without `SKIP_MOUNT` typically mounts:
20
+
21
+ ```ruby
22
+ mount CommandTower::Engine => "/api"
23
+ ```
24
+
25
+ or at `/`, depending on generator defaults. Confirm the file. Engine routes then live under that prefix (`GET /api/me` vs `GET /me`).
26
+
27
+ Use **one** mount. Do not mount the engine twice.
28
+
29
+ ### Host-owned healthz (`SKIP_MOUNT=1`)
30
+
31
+ Declare host routes **before** the engine so health checks never depend on engine routing:
32
+
33
+ ```ruby
34
+ Rails.application.routes.draw do
35
+ get "/api/healthz", to: "health#show"
36
+
37
+ mount CommandTower::Engine => "/api"
38
+ end
39
+ ```
40
+
41
+ `HealthController#show` is host-owned (liveness). Keep it trivial.
42
+
43
+ If the engine is mounted at `/`, a host `/healthz` still belongs **before** the mount.
44
+
45
+ ### Accidental public mount
46
+
47
+ If you mounted at `/` and needed `/api`, or the generator mounted when you needed to skip: fix `config/routes.rb` by hand. Re-running configure with `FORCE=1` is not the tool for route cleanup.
48
+
49
+ More on mount mistakes: [`../initializing.md`](../initializing.md) troubleshooting.
50
+
51
+ ## Stop
52
+
53
+ `config/routes.rb` has exactly one engine mount at the prefix your API and frontend will call. Host `healthz` (if any) is declared before that mount. Next: [`07-execution-bases.md`](07-execution-bases.md).
@@ -0,0 +1,38 @@
1
+ # 07 — Execution-boundary bases
2
+
3
+ ## Purpose
4
+
5
+ Host controllers and jobs inherit CommandTower bases so authentication, request context, envelope rendering, and job execution stay on the platform path.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`06-mount-and-health.md`](06-mount-and-health.md)
10
+
11
+ ## Files the host edits
12
+
13
+ | File | Inherit |
14
+ |------|---------|
15
+ | `app/controllers/application_controller.rb` | `CommandTower::ApplicationController` |
16
+ | `app/jobs/application_job.rb` | `CommandTower::ApplicationJob` |
17
+
18
+ ## Procedure
19
+
20
+ Replace the default `ActionController::API` / `ActiveJob::Base` superclasses:
21
+
22
+ ```ruby
23
+ class ApplicationController < CommandTower::ApplicationController
24
+ end
25
+ ```
26
+
27
+ ```ruby
28
+ class ApplicationJob < CommandTower::ApplicationJob
29
+ end
30
+ ```
31
+
32
+ Host controllers then subclass `ApplicationController` as usual. Do not subclass `ActionController::API` for product JSON that should share CommandTower auth and envelope behavior.
33
+
34
+ Background work invokes **workflows** (via CommandTower job helpers), not services as a full business action. See [`../architecture.md`](../architecture.md).
35
+
36
+ ## Stop
37
+
38
+ `ApplicationController` and `ApplicationJob` inherit CommandTower bases. Next: [`08-initializer.md`](08-initializer.md).
@@ -0,0 +1,90 @@
1
+ # 08 — Initializer
2
+
3
+ ## Purpose
4
+
5
+ Set **host_key**, secrets, local mail, and (when the client is Command Tower frontend on web) cookies. The generated file documents every class_composer option; do not uncomment the world.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`07-execution-bases.md`](07-execution-bases.md)
10
+ - Secrets from [`04-secrets-and-env.md`](04-secrets-and-env.md) already in Compose
11
+
12
+ ## Files the host edits
13
+
14
+ `config/initializers/command_tower.rb` (created by install)
15
+
16
+ In-engine dummy for **shape**: [`../../rails_app/config/initializers/command_tower.rb`](../../rails_app/config/initializers/command_tower.rb). Copy **intent** (host_key, secrets, later RBAC/gates), not the dummy’s engine-dev values as a product identity.
17
+
18
+ ## Procedure
19
+
20
+ ### `host_key` (required)
21
+
22
+ Server-bound host/product identity for CT-generic user-scoped state (experience states). **Not** client-supplied.
23
+
24
+ ```ruby
25
+ CommandTower.configure do |config|
26
+ config.application.host_key = "your_product"
27
+ end
28
+ ```
29
+
30
+ Blank `host_key` → Me experience-state routes return **503**.
31
+
32
+ The dummy sets `config.application.host_key = "command_tower"`. A product host uses its **own** key.
33
+
34
+ ### Secrets
35
+
36
+ Prefer ENV already injected by Compose:
37
+
38
+ ```ruby
39
+ config.jwt.hmac_secret = ENV.fetch("SECRET_KEY_BASE")
40
+ config.signup_session.jwt_secret = ENV.fetch("SIGNUP_SESSION_JWT_SECRET")
41
+ config.password_recovery_session.jwt_secret = ENV.fetch("PASSWORD_RECOVERY_SESSION_JWT_SECRET")
42
+ ```
43
+
44
+ Exact setter names are in the generated initializer comments and [`../initializing.md`](../initializing.md). Match those names; do not invent parallel keys.
45
+
46
+ ### Cookies / CSRF (required for Command Tower frontend on web)
47
+
48
+ Command Tower frontend `defaultSessionStrategy()` is **cookie + CSRF on web** and Bearer on native. If this API will serve that web client, enable cookies **now**. Curl-only smoke in [`12-smoke-check.md`](12-smoke-check.md) can still send `Authorization: Bearer`.
49
+
50
+ Contract and defaults: [`../cookie_authentication_guide.md`](../cookie_authentication_guide.md). Local HTTP Compose:
51
+
52
+ ```ruby
53
+ config.jwt.cookie.enabled = true
54
+ config.jwt.cookie.secure = false
55
+ config.jwt.cookie.same_site = :lax
56
+ config.jwt.cookie.csrf.enabled = true
57
+ ```
58
+
59
+ Do not set `secure = true` on `http://localhost`. Leave `domain` nil (host-only). Defaults are `ct_jwt` and `ct_csrf`. Full field table stays in the cookie guide.
60
+
61
+ A browser stores those cookies by hostname. The port is not part of the key, so two web apps on `localhost` share one `ct_jwt` and one `ct_csrf`. Logout expires both cookies. A second product on the same hostname sets both names, and that host’s frontend `session.csrfCookieName` matches the CSRF name. The header stays `X-CSRF-Token`. A host that runs alone leaves the defaults.
62
+
63
+ ```ruby
64
+ config.jwt.cookie.name = "other_jwt"
65
+ config.jwt.cookie.csrf.cookie_name = "other_csrf"
66
+ ```
67
+
68
+ If the only client is Bearer (`curl`, native-only), leave cookies disabled.
69
+
70
+ ### Email / SMTP
71
+
72
+ Step 02 already set `:file` delivery in `development.rb`. In the initializer, set the public app URL used in mail links (frontend origin, not the API port):
73
+
74
+ ```ruby
75
+ config.application.url = "http://localhost:8081"
76
+ ```
77
+
78
+ `8081` is one product’s Expo port. Use the port this host’s web app actually publishes, and put that same origin in `CORS_ALLOWED_ORIGINS` (step 01). Production SMTP is out of this manual’s deployment scope.
79
+
80
+ ### What not to do
81
+
82
+ - Do not duplicate CommandTower default hashes “for completeness”
83
+ - Do not set `FORCE=1` and regenerate after customizing
84
+ - Do not fork engine internals because a comment looked optional
85
+
86
+ RBAC default role and feature gates are steps 09–10.
87
+
88
+ ## Stop
89
+
90
+ `host_key` is a real product string. JWT and session secrets resolve from ENV. Cookie + CSRF are on if a CT frontend web client will call this API. Next: [`09-rbac.md`](09-rbac.md).
@@ -0,0 +1,83 @@
1
+ # 09 — RBAC
2
+
3
+ ## Purpose
4
+
5
+ Give authenticated users a **product role** that **grants CommandTower entity names**. Without this, login can succeed and `GET /me` returns **403**.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`08-initializer.md`](08-initializer.md)
10
+
11
+ ## Files the host creates
12
+
13
+ `config/rbac_groups.yml` — host-owned composition.
14
+
15
+ CommandTower ships **entity definitions** in `lib/command_tower/authorization/default.yml` (inside the gem). Hosts **grant those names**. Hosts may add host-owned entities for host controllers.
16
+
17
+ In-engine dummy for **shape**: [`../../rails_app/config/rbac_groups.yml`](../../rails_app/config/rbac_groups.yml). Use it to see a `member` grant list and that Admin bundles are **host policy**. Do not copy dummy-only entities (`dummy_admin_example`) into a product host.
18
+
19
+ Contract: [`../authorization.md`](../authorization.md), [`../authentication_authorization_guide.md`](../authentication_authorization_guide.md).
20
+
21
+ ## Procedure
22
+
23
+ ### 1. Author `member` (minimum for Me)
24
+
25
+ Grant the Me/Auth entity names your smoke path needs. A typical starting `member` (from the dummy, minus product-specific extras you do not want yet):
26
+
27
+ ```yaml
28
+ groups:
29
+ member:
30
+ description: Standard authenticated application user
31
+ entities:
32
+ - email_verification_send
33
+ - email_verification_verify
34
+ - session
35
+ - me
36
+ - profile
37
+ - me_name
38
+ - me_password
39
+ - me_account
40
+ - me_inbox
41
+ - me_audit_events
42
+ - me_preferences
43
+ - me_phone
44
+ - me_phone_verification_send
45
+ - me_phone_verification_verify
46
+ - me_pushover
47
+ - me_pushover_verification
48
+ - me_push
49
+ - me_experience_states
50
+ - principal_capabilities
51
+ ```
52
+
53
+ Trim unused Me surfaces if you want a thinner first grant (you **must** keep `me` and `session` for the smoke in [`12-smoke-check.md`](12-smoke-check.md)). Missing `me` → **403** on `GET /me`.
54
+
55
+ ### 2. `default_membership_role`
56
+
57
+ In `config/initializers/command_tower.rb`:
58
+
59
+ ```ruby
60
+ config.authorization.default_membership_role = "member"
61
+ ```
62
+
63
+ New users then receive `member` without a manual assign. If you omit this, you must assign roles some other supported way before Me works.
64
+
65
+ ### 3. Admin roles (not required to finish bootstrap)
66
+
67
+ Do **not** auto-grant new Admin capabilities to a generic Admin role. CommandTower does not ship an accumulating operational `admin` role. `owner` (`entities: true`) is full-access and distinct from host Admin bundles.
68
+
69
+ If you add Admin later, copy the **pattern** from the dummy (`audit_operator`, `operations_admin`, …): explicit entity lists on a host role. See [`../admin_workspace.md`](../admin_workspace.md).
70
+
71
+ The frontend workspace and user detail call these entities. Grant each one the role should have:
72
+
73
+ - `admin_workspace`
74
+ - `admin_users`
75
+ - `admin_audit_events`
76
+ - `admin_messaging_announcements`
77
+ - `admin_impersonation`
78
+
79
+ `admin_impersonation` is what shows Impersonate. The frontend does not add a route for it.
80
+
81
+ ## Stop
82
+
83
+ `config/rbac_groups.yml` exists with a `member` group that grants `me` (and the rest you need). `default_membership_role` is `"member"` (or you have a documented assign path). Next: [`10-roles-and-gates.md`](10-roles-and-gates.md).
@@ -0,0 +1,62 @@
1
+ # 10 — Roles and feature gates
2
+
3
+ ## Purpose
4
+
5
+ Assign the default membership role (if you did not in step 09) and **enable** the HTTP gates the smoke path needs. Gates default **off** → **404**.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`09-rbac.md`](09-rbac.md)
10
+
11
+ ## Files the host edits
12
+
13
+ `config/initializers/command_tower.rb`
14
+
15
+ Opening a `with_*` block typically sets its enable flag. Still set `enable` / `enabled` explicitly so a later comment-uncomment does not leave you on the default **off** path.
16
+
17
+ ## Procedure
18
+
19
+ ### Default role
20
+
21
+ ```ruby
22
+ config.authorization.default_membership_role = "member"
23
+ ```
24
+
25
+ ### Login / verify / reset (minimum for Me + CT frontend guest screens)
26
+
27
+ ```ruby
28
+ config.login.with_plain_text do |plain_text_config|
29
+ plain_text_config.enable = true
30
+
31
+ plain_text_config.with_email_verify do |email_verify_config|
32
+ email_verify_config.enable = true
33
+ # Curl smoke (step 12) can login before the user verifies.
34
+ # 0.minutes → GET /me is 412 until email is verified.
35
+ email_verify_config.verify_email_required_within = 7.days
36
+ end
37
+
38
+ plain_text_config.with_password_reset do |password_reset_config|
39
+ password_reset_config.enabled = true
40
+ end
41
+ end
42
+ ```
43
+
44
+ ### Availability (CT frontend signup)
45
+
46
+ If the guest signup screen checks username/email availability, enable those gates or the client sees **404**:
47
+
48
+ ```ruby
49
+ config.username.with_realtime_username_check do |realtime_username_check_config|
50
+ realtime_username_check_config.enable = true
51
+ end
52
+
53
+ config.signup_session.email_availability.enable = true
54
+ ```
55
+
56
+ Leave Admin and messaging gates off until you need those routes.
57
+
58
+ Do not use role-**name** checks in host code for authorization. Use entity grants / `allow_everything`. See [`../authorization.md`](../authorization.md).
59
+
60
+ ## Stop
61
+
62
+ Plain-text login is **on**. Register → login → `GET /me` is not **404**. Next: [`11-auth-client-path.md`](11-auth-client-path.md).
@@ -0,0 +1,57 @@
1
+ # 11 — Auth client path
2
+
3
+ ## Purpose
4
+
5
+ Pick **one** client authentication mode and configure the host to match. Mixing a cookie+CSRF frontend with cookies disabled will fail in the browser. `curl` can still send Bearer on the same host.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`10-roles-and-gates.md`](10-roles-and-gates.md)
10
+ - `gem "rack-cors"` from [`02-create-the-rails-app.md`](02-create-the-rails-app.md) if a browser will call this API
11
+
12
+ ## Two supported modes
13
+
14
+ | Mode | Client sends | Host initializer |
15
+ |------|----------------|------------------|
16
+ | Bearer | `Authorization: Bearer <jwt>` | Cookie JWT disabled |
17
+ | Cookie + CSRF | HttpOnly JWT cookie + CSRF header/cookie | Cookie + CSRF **enabled** (step 08) |
18
+
19
+ Command Tower **frontend** on web uses `defaultSessionStrategy()` → cookie + CSRF. Native uses Bearer. Enable cookies when that web client exists; keep Bearer working for `curl` and native.
20
+
21
+ Contract: [`../cookie_authentication_guide.md`](../cookie_authentication_guide.md).
22
+
23
+ Do not invent a third session store. Do not put JWTs in `localStorage` as a “simpler cookie.”
24
+
25
+ ## Procedure
26
+
27
+ 1. If the client is Command Tower frontend on web: cookies + CSRF already on from step 08 (`secure = false` on HTTP localhost).
28
+ 2. If Bearer-only: leave cookies disabled; step 12 uses `Authorization`.
29
+ 3. If a **browser on another origin** calls the API, add host CORS. CommandTower does not ship CORS.
30
+
31
+ `config/initializers/cors.rb`:
32
+
33
+ ```ruby
34
+ allowed_origins = ENV.fetch(
35
+ "CORS_ALLOWED_ORIGINS",
36
+ "http://localhost:8081,http://localhost:8082,http://localhost:19006"
37
+ ).split(",").map(&:strip).reject(&:empty?)
38
+
39
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
40
+ allow do
41
+ origins(*allowed_origins)
42
+ resource "*",
43
+ headers: :any,
44
+ methods: %i[get post put patch delete options head],
45
+ credentials: true,
46
+ expose: ["X-Authorization-Expire", "X-Authorization-Reset"]
47
+ end
48
+ end
49
+ ```
50
+
51
+ `credentials: true` requires an **explicit origin list**, not `*`. Compose already passes `CORS_ALLOWED_ORIGINS` from step 01. The sample origins `8081`, `8082`, and `19006` belong to one web product. Add the origin this Expo web app actually uses, and keep it aligned with `config.application.url` and the API’s published port.
52
+
53
+ Attaching a Command Tower frontend: [Local FE↔BE join](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md).
54
+
55
+ ## Stop
56
+
57
+ You can state which mode the smoke will use. CORS exists if a browser will call this API. Next: [`12-smoke-check.md`](12-smoke-check.md).
@@ -0,0 +1,81 @@
1
+ # 12 — Smoke check (`GET /me`)
2
+
3
+ ## Purpose
4
+
5
+ Prove the host is a usable Me/Auth API: register, log in, call `GET /me`, receive **200**.
6
+
7
+ ## Prerequisites
8
+
9
+ - Steps 00–11
10
+ - API listening: `make server` (or `make s`) — Puma bound on `0.0.0.0` (step 01)
11
+
12
+ Prefix every path with your engine mount (`/api` if you mounted there). Examples below assume mount at `/api` and host port **3000**.
13
+
14
+ HTTP catalog (full contract): [`../api_reference.md`](../api_reference.md). This page is the **happy path only**.
15
+
16
+ ## Procedure
17
+
18
+ Run `curl` **on the Mac** against the published Compose port. That is HTTP, not Ruby on the host.
19
+
20
+ Login succeeds with **201** (not 200). Register succeeds with **201** and **no** token.
21
+
22
+ ### 1. Register
23
+
24
+ ```bash
25
+ curl -sS -X POST http://localhost:3000/api/auth/register \
26
+ -H "Content-Type: application/json" \
27
+ -d '{
28
+ "first_name": "Ada",
29
+ "last_name": "Lovelace",
30
+ "username": "ada1",
31
+ "email": "ada@example.com",
32
+ "password": "password12",
33
+ "password_confirmation": "password12"
34
+ }'
35
+ ```
36
+
37
+ Expect **201**. No JWT. If email verification is required immediately (`verify_email_required_within = 0.minutes`), either verify using the code in `tmp/mails` or raise the grace period (step 10).
38
+
39
+ If you already have a user in the database, skip to login.
40
+
41
+ ### 2. Login
42
+
43
+ ```bash
44
+ curl -sS -X POST http://localhost:3000/api/auth/plain-text/login \
45
+ -H "Content-Type: application/json" \
46
+ -d '{"identifier": "ada@example.com", "password": "password12"}'
47
+ ```
48
+
49
+ Expect **201** and `data.token`. Cookie mode also sets `Set-Cookie` for `ct_jwt` (and `ct_csrf` when CSRF is on).
50
+
51
+ ### 3. Me
52
+
53
+ ```bash
54
+ TOKEN='<paste data.token>'
55
+ curl -sS -H "Authorization: Bearer ${TOKEN}" http://localhost:3000/api/me
56
+ ```
57
+
58
+ Expect **200** and a Me payload.
59
+
60
+ Cookie mode: omit `Authorization`, send `-b cookies.txt` after login with `-c cookies.txt`, and on mutating requests add `-H "X-CSRF-Token: …"` from the CSRF cookie. `GET /me` still needs the JWT cookie.
61
+
62
+ ## How to read failures
63
+
64
+ | Status | Meaning |
65
+ |--------|---------|
66
+ | **401** | Not authenticated — missing/invalid token or cookie |
67
+ | **403** | Authenticated but **not authorized** — RBAC grant missing (`me` not on the user’s role) |
68
+ | **404** | Feature gate off or wrong path/mount |
69
+ | **412** | Email verification required — grace period elapsed; verify or raise `verify_email_required_within` |
70
+ | **503** | Often blank `host_key` on experience-state routes; fix step 08 |
71
+ | Connection refused | Compose `command` not binding `0.0.0.0`, or published port mismatch |
72
+
73
+ Do not “fix” 403 by disabling RBAC. Grant `me` on `member`.
74
+
75
+ ## After `GET /me`
76
+
77
+ Attaching a Command Tower frontend is **not** this file. Follow [Local FE↔BE join](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md).
78
+
79
+ ## Stop
80
+
81
+ `GET /me` is **200** for a `member` user. Optional extras: [`13-optional.md`](13-optional.md). Required wrap-up: [`14-sanity-checks.md`](14-sanity-checks.md).
@@ -0,0 +1,50 @@
1
+ # 13 — Optional (messaging, tests, client-version compatibility)
2
+
3
+ ## Purpose
4
+
5
+ After `GET /me` works, add platform extras **only if the product needs them**. A platform-proof host may skip this file and run [`14-sanity-checks.md`](14-sanity-checks.md).
6
+
7
+ ## Prerequisites
8
+
9
+ - [`12-smoke-check.md`](12-smoke-check.md) passed
10
+
11
+ Do not start here. Messaging and CVC assume install, RBAC, and gates already exist.
12
+
13
+ ## Messaging
14
+
15
+ When the host **emits** notifications or uses phone/Pushover:
16
+
17
+ - Catalog, channel policy, adapters, credentials: [`../messaging_integration_guide.md`](../messaging_integration_guide.md)
18
+ - Admin announcements: [`../admin_workspace.md`](../admin_workspace.md)
19
+
20
+ Complete steps 00–12 first. This bootstrap tree does not duplicate the messaging guide.
21
+
22
+ ## Host tests
23
+
24
+ Shared FactoryBot definitions ship with the gem. In the host test boot path:
25
+
26
+ ```ruby
27
+ require "command_tower/testing"
28
+ CommandTower::Testing.install!
29
+ ```
30
+
31
+ Then `FactoryBot.modify` as needed. Contract: [`../testing.md`](../testing.md).
32
+
33
+ Run specs **in Compose**:
34
+
35
+ ```bash
36
+ make rspec
37
+ make spec SPEC=spec/path/to/file_spec.rb
38
+ ```
39
+
40
+ Do not run `rspec` on the Mac.
41
+
42
+ ## Client-version compatibility (CVC)
43
+
44
+ If the frontend must block incompatible native/web builds, register CVC in the host (registry / initializer). Frontend recovery UX is a client concern; the backend must serve the compatibility contract the client expects.
45
+
46
+ Follow the gem’s registry comments and [`../extending.md`](../extending.md) for `config.registry` patterns. Do not invent a parallel version table.
47
+
48
+ ## Stop
49
+
50
+ Either you skipped this step, or messaging/tests/CVC point at specialty docs and run via Make. Next: [`14-sanity-checks.md`](14-sanity-checks.md).
@@ -0,0 +1,44 @@
1
+ # 14 — Sanity checks
2
+
3
+ ## Purpose
4
+
5
+ Stop condition for **local** bootstrap. This is not a production launch checklist.
6
+
7
+ ## Prerequisites
8
+
9
+ - Steps 00–12 complete
10
+ - Step 13 only if you opted in
11
+
12
+ ## Checks
13
+
14
+ Run inside Compose (Make wrappers from [`01-docker-make-compose.md`](01-docker-make-compose.md)):
15
+
16
+ | Check | Command | Expect |
17
+ |-------|---------|--------|
18
+ | Doctor | `make doctor` | Exit 0 |
19
+ | Server | `make server` | API listens on the Compose port |
20
+ | Me smoke | [`12-smoke-check.md`](12-smoke-check.md) | `GET /me` **200** |
21
+ | Host specs (if you added them) | `make rspec` | Pass |
22
+ | RuboCop (if you added it) | `make rubocop` | Pass |
23
+
24
+ 401 vs 403 vs 404: still [`12-smoke-check.md`](12-smoke-check.md).
25
+
26
+ ## Not this manual
27
+
28
+ Do not treat the following as remaining bootstrap steps:
29
+
30
+ - Production deploys, systemd, reverse proxies, LXC
31
+ - Production secret distribution
32
+ - Multi-host clustering
33
+ - App-store / frontend release (see the Command Tower **frontend** bootstrap tree)
34
+
35
+ ## After this tree
36
+
37
+ - Product routes and workflows: [`../extending.md`](../extending.md), [`../architecture.md`](../architecture.md)
38
+ - HTTP you did not smoke: [`../api_reference.md`](../api_reference.md)
39
+ - Gem upgrades: [`../upgrades/README.md`](../upgrades/README.md)
40
+ - Command Tower frontend on this API: [Local FE↔BE join](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md)
41
+
42
+ ## Stop
43
+
44
+ Doctor is green and `GET /me` is **200**. Local Command Tower backend bootstrap is complete.
@@ -0,0 +1,90 @@
1
+ # Bootstrap a new Command Tower backend host
2
+
3
+ Bring up a new Rails API host that **mounts** CommandTower, in order.
4
+
5
+ This directory is the host bring-up authority. Specialty contracts stay in sibling docs (`initializing`, `authorization`, `api_reference`, `extending`). This tree owns **order**, **which files the host authors** (including Docker and Make), and **how to invoke Rails** — always inside Compose.
6
+
7
+ Do **not** copy another product host’s tree. Do **not** copy [`../docker-compose.yaml`](../docker-compose.yaml) from this gem (that file is for **engine development**). The dummy app under [`../../rails_app/`](../../rails_app/) is an in-engine example for initializer and RBAC shape, not a product template.
8
+
9
+ ## Docker-only
10
+
11
+ Every Ruby/Rails action runs inside Docker Compose.
12
+
13
+ **Host-machine tools:** Docker, the Docker Compose plugin, Make, Git. That is the entire local toolchain.
14
+
15
+ Do **not** run on the Mac (or any unsandboxed host OS):
16
+
17
+ - `gem install rails`
18
+ - `rails new`
19
+ - `bundle`
20
+ - `bin/rails` / `bin/rake`
21
+ - `rspec` / `rubocop`
22
+
23
+ Those names appear in this tree as **task names** inside the container, always invoked as `make …`.
24
+
25
+ **Chicken-and-egg:** author `Dockerfile`, `docker-compose.yml`, and `Makefile` **first** ([`01-docker-make-compose.md`](01-docker-make-compose.md)). `make build` produces a Ruby image. `make rails-new` runs `rails new` **in that image**. Only then pin the gem and install CommandTower.
26
+
27
+ ## This is not deployment
28
+
29
+ This manual is **local bring-up**. It ends when the host runs in Compose and passes the checks in [`14-sanity-checks.md`](14-sanity-checks.md): `make doctor` and `GET /me` **200** for a member user.
30
+
31
+ It does **not** cover production deploys, systemd, reverse proxies, LXC, or how secrets are distributed in production. Do not add a deploy step to this tree without a separate decision.
32
+
33
+ ## How to read
34
+
35
+ 1. Follow **Implementation order** below. That list is the only sequence.
36
+ 2. Open one numbered file per step. Later files assume earlier steps exist.
37
+ 3. Each step states purpose, prerequisites, files to create, the procedure (`make` only), and the stop condition.
38
+ 4. For field contracts, follow the linked specialty doc. Do not treat this tree as a second copy of those contracts.
39
+
40
+ A **platform-proof** host is: Docker/Make, Rails API in the container, CommandTower installed, `host_key`, host RBAC grants, feature gates, register → login → `GET /me`. Product domain and messaging adapters are optional ([`13-optional.md`](13-optional.md)).
41
+
42
+ Attaching a Command Tower **frontend** is a separate step after this tree: [Local FE↔BE join](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md).
43
+
44
+ ## Implementation order
45
+
46
+ | Step | File | What you do |
47
+ |------|------|-------------|
48
+ | 00 | [`00-ownership.md`](00-ownership.md) | Engine vs host; schema; Docker-only; dummy is not a product template |
49
+ | 01 | [`01-docker-make-compose.md`](01-docker-make-compose.md) | Dockerfile, Compose, Makefile, `.dockerignore` |
50
+ | 02 | [`02-create-the-rails-app.md`](02-create-the-rails-app.md) | `make rails-new` (Rails 8 API) inside the container |
51
+ | 03 | [`03-pin-the-gem.md`](03-pin-the-gem.md) | `gem "command_tower"` from RubyGems (or git tag) |
52
+ | 04 | [`04-secrets-and-env.md`](04-secrets-and-env.md) | JWT / signup-session / password-recovery secrets in Compose |
53
+ | 05 | [`05-install-migrate-doctor.md`](05-install-migrate-doctor.md) | `command_tower:install`, migrate, doctor |
54
+ | 06 | [`06-mount-and-health.md`](06-mount-and-health.md) | Engine mount path; host healthz |
55
+ | 07 | [`07-execution-bases.md`](07-execution-bases.md) | Inherit CommandTower controller/job bases |
56
+ | 08 | [`08-initializer.md`](08-initializer.md) | `host_key`, cookies if needed, email knobs |
57
+ | 09 | [`09-rbac.md`](09-rbac.md) | Host `rbac_groups.yml` grants CT entity names |
58
+ | 10 | [`10-roles-and-gates.md`](10-roles-and-gates.md) | Default `member`; enable login / verify / reset gates |
59
+ | 11 | [`11-auth-client-path.md`](11-auth-client-path.md) | Bearer vs cookie+CSRF |
60
+ | 12 | [`12-smoke-check.md`](12-smoke-check.md) | Register → login → `GET /me` **200** |
61
+ | 13 | [`13-optional.md`](13-optional.md) | Optional: messaging, host tests, client-version compatibility |
62
+ | 14 | [`14-sanity-checks.md`](14-sanity-checks.md) | Stop: doctor + Me smoke; not deployment |
63
+
64
+ Step 13 is optional after 12.
65
+
66
+ ## File index
67
+
68
+ Same files as the table. Filename numbers match the order list. If you add, remove, or reorder a step, change this README and the filenames together.
69
+
70
+ ## Specialty docs this tree does not replace
71
+
72
+ | Need | Doc |
73
+ |------|-----|
74
+ | Attach a Command Tower frontend locally | [`../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md`](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md) |
75
+ | What `command_tower:install` does, flags, schema, doctor | [`../initializing.md`](../initializing.md) |
76
+ | Layer map, workflow rule | [`../architecture.md`](../architecture.md) |
77
+ | Extension points | [`../extending.md`](../extending.md) |
78
+ | HTTP catalog | [`../api_reference.md`](../api_reference.md) |
79
+ | Route areas | [`../controllers.md`](../controllers.md) |
80
+ | RBAC | [`../authorization.md`](../authorization.md), [`../authentication_authorization_guide.md`](../authentication_authorization_guide.md) |
81
+ | Cookie / CSRF | [`../cookie_authentication_guide.md`](../cookie_authentication_guide.md) |
82
+ | Messaging | [`../messaging_integration_guide.md`](../messaging_integration_guide.md) |
83
+ | Testing factories | [`../testing.md`](../testing.md) |
84
+ | Admin Workspace | [`../admin_workspace.md`](../admin_workspace.md) |
85
+ | Principal capabilities | [`../principal_capabilities.md`](../principal_capabilities.md) |
86
+ | Audit registry | [`../audit.md`](../audit.md) |
87
+ | Eventing | [`../eventing.md`](../eventing.md) |
88
+ | Release upgrades | [`../upgrades/README.md`](../upgrades/README.md) |
89
+
90
+ Older entry: [`../host_integration_guide.md`](../host_integration_guide.md) is a pointer here.
@@ -37,6 +37,17 @@ end
37
37
  | CSRF `same_site` / `secure` / `path` / `domain` | `nil` (inherit JWT cookie) |
38
38
  | CSRF `ttl` | `7.days` |
39
39
 
40
+ ### Two web apps on one hostname
41
+
42
+ The browser keys `ct_jwt` and `ct_csrf` by hostname. The port is not part of that key. Two Command Tower web apps on `localhost` overwrite each other’s session. Logout expires both cookies, so a shared CSRF name still logs the other app out on its next POST.
43
+
44
+ A second product sets both names. The frontend `session.csrfCookieName` matches `csrf.cookie_name`. Leave `header_name` as `X-CSRF-Token`. A host that runs alone keeps `ct_jwt` and `ct_csrf`.
45
+
46
+ ```ruby
47
+ config.jwt.cookie.name = "other_jwt"
48
+ config.jwt.cookie.csrf.cookie_name = "other_csrf"
49
+ ```
50
+
40
51
  ## Token extraction order
41
52
 
42
53
  1. `Authorization: Bearer <token>` header (wins when present)