flow_chat 0.8.2 → 0.10.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.cliff.toml +74 -0
  3. data/.github/workflows/ci.yml +2 -3
  4. data/.github/workflows/pages.yml +43 -0
  5. data/.github/workflows/release.yml +56 -0
  6. data/.standard.yml +4 -0
  7. data/CHANGELOG.md +48 -0
  8. data/CLAUDE.md +327 -0
  9. data/CONTRIBUTING.md +134 -0
  10. data/Gemfile +1 -0
  11. data/README.md +189 -133
  12. data/Rakefile +17 -2
  13. data/SECURITY.md +42 -349
  14. data/docs/architecture.md +83 -0
  15. data/docs/async-background-processing.md +64 -0
  16. data/docs/configuration.md +110 -287
  17. data/docs/factory-pattern.md +58 -0
  18. data/docs/gateway-context-variables.md +168 -0
  19. data/docs/gateway-development.md +159 -0
  20. data/docs/getting-started.md +90 -0
  21. data/docs/instrumentation.md +95 -175
  22. data/docs/platforms/instagram.md +278 -0
  23. data/docs/platforms/messenger.md +205 -0
  24. data/docs/platforms/telegram.md +109 -0
  25. data/docs/platforms/ussd.md +78 -0
  26. data/docs/platforms/whatsapp.md +147 -0
  27. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  28. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  29. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  30. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  31. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  32. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  33. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  34. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  35. data/docs/testing.md +33 -426
  36. data/examples/custom_session_id_example.rb +119 -0
  37. data/examples/http_controller.rb +22 -20
  38. data/examples/intercom_configuration_example.rb +113 -0
  39. data/examples/intercom_controller.rb +182 -0
  40. data/examples/multi_tenant_whatsapp_controller.rb +63 -168
  41. data/examples/simulator_controller.rb +0 -1
  42. data/examples/ussd_controller.rb +88 -160
  43. data/examples/whatsapp_controller.rb +18 -17
  44. data/examples/whatsapp_media_examples.rb +27 -79
  45. data/flow_chat.gemspec +4 -0
  46. data/lib/flow_chat/app.rb +211 -0
  47. data/lib/flow_chat/async_job.rb +176 -0
  48. data/lib/flow_chat/choice_titles.rb +95 -0
  49. data/lib/flow_chat/config.rb +126 -23
  50. data/lib/flow_chat/delivery_error.rb +9 -0
  51. data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
  52. data/lib/flow_chat/factory.rb +94 -0
  53. data/lib/flow_chat/gateway_async_support.rb +106 -0
  54. data/lib/flow_chat/generic_async_job.rb +30 -0
  55. data/lib/flow_chat/http/configuration_error.rb +9 -0
  56. data/lib/flow_chat/http/gateway/simple.rb +104 -36
  57. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  58. data/lib/flow_chat/http/renderer.rb +3 -3
  59. data/lib/flow_chat/input.rb +86 -0
  60. data/lib/flow_chat/instagram/client.rb +32 -0
  61. data/lib/flow_chat/instagram/configuration.rb +147 -0
  62. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  63. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  64. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  65. data/lib/flow_chat/instagram/renderer.rb +23 -0
  66. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  67. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  68. data/lib/flow_chat/instrumentation.rb +182 -0
  69. data/lib/flow_chat/intercom/client.rb +161 -0
  70. data/lib/flow_chat/intercom/configuration.rb +102 -0
  71. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  72. data/lib/flow_chat/intercom/gateway/intercom_api.rb +420 -0
  73. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  74. data/lib/flow_chat/intercom/renderer.rb +123 -0
  75. data/lib/flow_chat/media.rb +121 -0
  76. data/lib/flow_chat/messenger/client.rb +264 -0
  77. data/lib/flow_chat/messenger/configuration.rb +103 -0
  78. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  79. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  80. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  81. data/lib/flow_chat/messenger/renderer.rb +150 -0
  82. data/lib/flow_chat/meta/challenge.rb +24 -0
  83. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  84. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  85. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  86. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  87. data/lib/flow_chat/meta/signature.rb +30 -0
  88. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  89. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  90. data/lib/flow_chat/named_configuration.rb +65 -0
  91. data/lib/flow_chat/phone_number_util.rb +37 -35
  92. data/lib/flow_chat/processor.rb +188 -0
  93. data/lib/flow_chat/prompt.rb +13 -16
  94. data/lib/flow_chat/renderers/markdown_support.rb +167 -0
  95. data/lib/flow_chat/security.rb +76 -0
  96. data/lib/flow_chat/session/middleware.rb +36 -11
  97. data/lib/flow_chat/simulator/controller.rb +31 -15
  98. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  99. data/lib/flow_chat/telegram/client.rb +283 -0
  100. data/lib/flow_chat/telegram/configuration.rb +78 -0
  101. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  102. data/lib/flow_chat/telegram/gateway/bot_api.rb +318 -0
  103. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +96 -0
  104. data/lib/flow_chat/telegram/renderer.rb +133 -0
  105. data/lib/flow_chat/telegram.rb +7 -0
  106. data/lib/flow_chat/text_truncator.rb +75 -0
  107. data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
  108. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  109. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  110. data/lib/flow_chat/ussd/renderer.rb +1 -1
  111. data/lib/flow_chat/version.rb +1 -1
  112. data/lib/flow_chat/whatsapp/client.rb +158 -20
  113. data/lib/flow_chat/whatsapp/configuration.rb +13 -52
  114. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  115. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +335 -248
  116. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +234 -0
  117. data/lib/flow_chat/whatsapp/renderer.rb +259 -64
  118. data/lib/flow_chat.rb +1 -1
  119. data/lib/tasks/release.rake +165 -0
  120. data/site/.nojekyll +0 -0
  121. data/site/.og-card.html +89 -0
  122. data/site/favicon.svg +6 -0
  123. data/site/index.html +209 -0
  124. data/site/og.png +0 -0
  125. metadata +132 -25
  126. data/docs/flows.md +0 -320
  127. data/docs/http-gateway-protocol.md +0 -432
  128. data/docs/images/simulator.png +0 -0
  129. data/docs/media.md +0 -153
  130. data/docs/sessions.md +0 -433
  131. data/docs/ussd-setup.md +0 -322
  132. data/docs/whatsapp-setup.md +0 -162
  133. data/examples/whatsapp_message_job.rb +0 -113
  134. data/lib/flow_chat/base_app.rb +0 -86
  135. data/lib/flow_chat/base_processor.rb +0 -146
  136. data/lib/flow_chat/http/app.rb +0 -6
  137. data/lib/flow_chat/http/middleware/executor.rb +0 -24
  138. data/lib/flow_chat/http/processor.rb +0 -33
  139. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  140. data/lib/flow_chat/ussd/app.rb +0 -6
  141. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
  142. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  143. data/lib/flow_chat/ussd/processor.rb +0 -39
  144. data/lib/flow_chat/whatsapp/app.rb +0 -29
  145. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  146. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  147. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
@@ -1,360 +1,183 @@
1
- # Configuration Reference
1
+ # Configuration
2
2
 
3
- This document covers all FlowChat configuration options.
3
+ FlowChat has two layers of configuration: global settings on `FlowChat::Config`, set once during boot, and per-processor settings passed to the `FlowChat::Processor.new` block for each webhook.
4
4
 
5
- ## Framework Configuration
5
+ ## Global configuration
6
6
 
7
- ```ruby
8
- # config/initializers/flowchat.rb
9
-
10
- # Core configuration
11
- FlowChat::Config.logger = Rails.logger
12
- FlowChat::Config.cache = Rails.cache
13
- FlowChat::Config.simulator_secret = "your_secure_secret_here"
14
-
15
- # Validation error display behavior
16
- FlowChat::Config.combine_validation_error_with_message = true # default
7
+ Set these once, for example in `config/initializers/flow_chat.rb`.
17
8
 
18
- # Setup instrumentation (optional)
19
- FlowChat.setup_instrumentation!
20
- ```
21
-
22
- ## Session Configuration
9
+ | Option | Default | What it controls |
10
+ |---|---|---|
11
+ | `FlowChat::Config.logger` | `Logger.new($stdout)` | The logger FlowChat writes to. Set it to `Rails.logger` to fold FlowChat logs into your app's. |
12
+ | `FlowChat::Config.cache` | `nil` | The cache backend the session store reads and writes. Required: `CacheSessionStore` raises without it. Set it to `Rails.cache` or any store with the same interface. |
13
+ | `FlowChat::Config.simulator_secret` | `nil` | Secret that enables the local web simulator. The simulator stays off until this is set. See [testing.md](testing.md). |
14
+ | `FlowChat::Config.combine_validation_error_with_message` | `true` | When `true`, a rejected input re-prompts with the validation error followed by the original prompt. When `false`, only the error is shown. |
15
+ | `FlowChat::Config.inject_middleware_logger` | `true` in Rails development, else `false` | Whether a logging middleware is inserted into the stack automatically. |
23
16
 
24
17
  ```ruby
25
- # Session boundaries control how session IDs are constructed
26
- FlowChat::Config.session.boundaries = [:flow, :platform] # default
27
- FlowChat::Config.session.hash_identifiers = true # hash identifiers for privacy
28
- FlowChat::Config.session.identifier = nil # let platforms choose (default)
29
-
30
- # Available boundary options:
31
- # :flow - separate sessions per flow class
32
- # :platform - separate sessions per platform (ussd, whatsapp)
33
- # :gateway - separate sessions per gateway
34
- # [] - global sessions (no boundaries)
35
-
36
- # Available identifier options:
37
- # nil - platform chooses default (:request_id for USSD, :msisdn for WhatsApp)
38
- # :msisdn - use phone number (durable sessions)
39
- # :request_id - use request ID (ephemeral sessions)
18
+ # config/initializers/flow_chat.rb
19
+ FlowChat::Config.cache = Rails.cache
20
+ FlowChat::Config.logger = Rails.logger
21
+ FlowChat::Config.simulator_secret = Rails.application.credentials.flow_chat_simulator_secret
40
22
  ```
41
23
 
42
- ## USSD Configuration
24
+ ## USSD configuration
43
25
 
44
- ```ruby
45
- # USSD pagination settings
46
- FlowChat::Config.ussd.pagination_page_size = 140 # characters per page
47
- FlowChat::Config.ussd.pagination_next_option = "#" # option to go to next page
48
- FlowChat::Config.ussd.pagination_next_text = "More" # text for next option
49
- FlowChat::Config.ussd.pagination_back_option = "0" # option to go back
50
- FlowChat::Config.ussd.pagination_back_text = "Back" # text for back option
51
- ```
26
+ `FlowChat::Config.ussd` controls how USSD responses are paginated. USSD messages are length-limited, so FlowChat splits long output into pages and adds navigation options.
52
27
 
53
- ## WhatsApp Configuration
28
+ | Option | Default | What it controls |
29
+ |---|---|---|
30
+ | `pagination_page_size` | `140` | Maximum characters per USSD page before FlowChat splits the response. |
31
+ | `pagination_next_option` | `"#"` | The input a user sends to see the next page. |
32
+ | `pagination_next_text` | `"More"` | The label shown next to the next-page option. |
33
+ | `pagination_back_option` | `"0"` | The input a user sends to go to the previous page. |
34
+ | `pagination_back_text` | `"Back"` | The label shown next to the previous-page option. |
54
35
 
55
36
  ```ruby
56
- # Message handling modes
57
- FlowChat::Config.whatsapp.message_handling_mode = :inline # :inline, :background, :simulator
58
- FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
37
+ FlowChat::Config.ussd.pagination_page_size = 160
59
38
  ```
60
39
 
61
- ### WhatsApp Credential Configuration
40
+ ## WhatsApp configuration
62
41
 
63
- #### Option 1: Rails Credentials
42
+ `FlowChat::Config.whatsapp` holds the WhatsApp Cloud API base URL. Per-tenant credentials (access token, phone number id, app secret, verify token) are passed to the gateway, not set here. See [platforms/whatsapp.md](platforms/whatsapp.md).
64
43
 
65
- ```bash
66
- rails credentials:edit
67
- ```
44
+ | Option | Default | What it controls |
45
+ |---|---|---|
46
+ | `api_base_url` | `"https://graph.facebook.com/v23.0"` | The Cloud API version and host the WhatsApp client calls. |
68
47
 
69
- ```yaml
70
- whatsapp:
71
- access_token: "your_access_token"
72
- phone_number_id: "your_phone_number_id"
73
- verify_token: "your_verify_token"
74
- app_id: "your_app_id"
75
- app_secret: "your_app_secret"
76
- business_account_id: "your_business_account_id"
77
- skip_signature_validation: false
78
- ```
48
+ ## HTTP configuration
79
49
 
80
- #### Option 2: Environment Variables
50
+ `FlowChat::Config.http` holds defaults for the HTTP gateway used in testing and custom integrations. See the platform guides for the full request and response shapes.
81
51
 
82
- ```bash
83
- export WHATSAPP_ACCESS_TOKEN="your_access_token"
84
- export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"
85
- export WHATSAPP_VERIFY_TOKEN="your_verify_token"
86
- export WHATSAPP_APP_ID="your_app_id"
87
- export WHATSAPP_APP_SECRET="your_app_secret"
88
- export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
89
- export WHATSAPP_SKIP_SIGNATURE_VALIDATION="false"
90
- ```
52
+ | Option | Default | What it controls |
53
+ |---|---|---|
54
+ | `default_gateway` | `:simple` | The HTTP gateway used when none is named. |
55
+ | `request_timeout` | `30` | Request timeout in seconds. |
56
+ | `response_format` | `:json` | The response serialization format. |
91
57
 
92
- #### Option 3: Programmatic Configuration
58
+ ## Sessions
93
59
 
94
- ```ruby
95
- config = FlowChat::Whatsapp::Configuration.new(:my_config) # Named configuration
96
- config.access_token = "your_access_token"
97
- config.phone_number_id = "your_phone_number_id"
98
- config.verify_token = "your_verify_token"
99
- config.app_id = "your_app_id"
100
- config.app_secret = "your_app_secret"
101
- config.business_account_id = "your_business_account_id"
102
- config.skip_signature_validation = false
103
- # Configuration is automatically registered as :my_config
104
- ```
60
+ A session holds the answers a flow has collected so far. FlowChat looks the session up by an id it builds on every request from two things: a set of boundaries and an identifier.
105
61
 
106
- **⚠️ Important for Background Jobs:** When using background mode with programmatic configurations, you must register them in an initializer:
62
+ `FlowChat::Config.session` sets the defaults.
107
63
 
108
- ```ruby
109
- # config/initializers/whatsapp_configs.rb
110
- # Register configurations so background jobs can access them
111
- production_config = FlowChat::Whatsapp::Configuration.new(:production)
112
- production_config.access_token = ENV['PROD_WHATSAPP_TOKEN']
113
- # ... other settings
114
-
115
- staging_config = FlowChat::Whatsapp::Configuration.new(:staging)
116
- staging_config.access_token = ENV['STAGING_WHATSAPP_TOKEN']
117
- # ... other settings
118
- ```
64
+ | Option | Default | What it controls |
65
+ |---|---|---|
66
+ | `boundaries` | `[:flow, :gateway, :platform]` | Which dimensions separate one session from another (see below). |
67
+ | `hash_identifiers` | `true` | Whether the identifier (often a phone number) is hashed into the session id rather than stored in the clear. |
68
+ | `identifier` | `nil` | Which request value identifies the user. `nil` lets the platform choose: WhatsApp uses `:msisdn`, the others use `:request_id`. |
69
+ | `session_id_proc` | `nil` | A callable that builds the session id from the context directly, bypassing boundaries and identifier. |
119
70
 
120
- Then use named configurations in controllers:
71
+ ### How the session id is built
121
72
 
122
- ```ruby
123
- # Use registered configuration
124
- config = FlowChat::Whatsapp::Configuration.get(:production)
125
- processor = FlowChat::Whatsapp::Processor.new(self) do |config|
126
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, config
127
- end
128
- ```
129
-
130
- ## Security Configuration
73
+ FlowChat sets the session id one of three ways, in order of precedence:
131
74
 
132
- ### WhatsApp Security
133
-
134
- ```ruby
135
- # Production security (recommended)
136
- config.app_secret = "your_whatsapp_app_secret"
137
- config.skip_signature_validation = false # default
75
+ 1. If `context["session.id"]` is already set, that value is used verbatim (a manual override).
76
+ 2. If you passed a block to `use_session_config`, its return value is used verbatim. Boundaries and identifier are skipped entirely.
77
+ 3. Otherwise the id is built from the boundaries and the identifier, described below.
138
78
 
139
- # Development mode (disable validation)
140
- config.app_secret = nil
141
- config.skip_signature_validation = true
142
- ```
143
-
144
- ### Simulator Security
145
-
146
- ```ruby
147
- # Use Rails secret for uniqueness
148
- FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
79
+ Think of the built id as `boundaries` followed by `identifier`: the boundaries describe the context a session lives in (the walls that separate it), and the identifier is who the session belongs to. The parts are joined with `:` in this fixed order, and a part is included only when its boundary is enabled and its value is present:
149
80
 
150
- # Or use dedicated secret
151
- FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
152
81
  ```
153
-
154
- ## Environment-Specific Configuration
155
-
156
- ```ruby
157
- # config/initializers/flowchat.rb
158
- case Rails.env
159
- when 'development'
160
- FlowChat::Config.whatsapp.message_handling_mode = :simulator
161
- FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
162
-
163
- when 'test'
164
- FlowChat::Config.whatsapp.message_handling_mode = :simulator
165
- FlowChat::Config.simulator_secret = "test_secret"
166
-
167
- when 'staging'
168
- FlowChat::Config.whatsapp.message_handling_mode = :inline
169
- FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
170
-
171
- when 'production'
172
- FlowChat::Config.whatsapp.message_handling_mode = :background
173
- FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
174
- FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
175
- end
82
+ flow_name : platform : gateway : url : identifier
176
83
  ```
177
84
 
178
- ## Processor Configuration
179
-
180
- ### USSD Processor
85
+ The order of the parts is fixed regardless of the order you list boundaries in. With the default boundaries `[:flow, :gateway, :platform]`, a WhatsApp user in `RegistrationFlow` gets:
181
86
 
182
- ```ruby
183
- processor = FlowChat::Ussd::Processor.new(self) do |config|
184
- # Gateway (required)
185
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
186
-
187
- # Session storage (required)
188
- config.use_session_store FlowChat::Session::CacheSessionStore
189
-
190
- # Optional middleware
191
- config.use_middleware MyCustomMiddleware
192
-
193
- # Configure session boundaries
194
- config.use_session_config(
195
- boundaries: [:flow, :platform], # which boundaries to enforce
196
- hash_identifiers: true, # hash identifiers for privacy
197
- identifier: :msisdn # use MSISDN for durable sessions (optional)
198
- )
199
-
200
- # Shorthand for durable sessions (identifier: :msisdn)
201
- config.use_durable_sessions
202
- end
203
87
  ```
204
-
205
- ### WhatsApp Processor
206
-
207
- ```ruby
208
- processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
209
- # Gateway (required)
210
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
211
-
212
- # Session storage (required)
213
- config.use_session_store FlowChat::Session::CacheSessionStore
214
-
215
- # Optional custom configuration
216
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_whatsapp_config
217
- end
88
+ registration_flow:whatsapp:whatsapp_cloud_api:9f86d0818...
89
+ (flow) (platform) (gateway) (hashed msisdn)
218
90
  ```
219
91
 
220
- ## Session Store Options
92
+ The hashed identifier is shown truncated here for readability; the real key carries the 32-character (128-bit) SHA256 prefix.
221
93
 
222
- ### Cache Session Store
94
+ ### What each boundary isolates
223
95
 
224
- ```ruby
225
- config.use_session_store FlowChat::Session::CacheSessionStore
226
- ```
96
+ A boundary is a wall. Include it and two requests that differ on that dimension get separate sessions; drop it and they share one session, provided the identifier matches.
227
97
 
228
- Uses Rails cache backend with automatic TTL management. This is the primary session store available in FlowChat.
98
+ | Boundary | Segment | Adding it separates sessions by | Drop it when |
99
+ |---|---|---|---|
100
+ | `:flow` | `context["flow.name"]` | flow class, so `RegistrationFlow` and `SurveyFlow` never share state | you want one session shared across all flows (rare) |
101
+ | `:platform` | `:ussd`, `:whatsapp`, and so on | platform, so the same person on USSD and on WhatsApp gets two sessions | you want one conversation to span platforms |
102
+ | `:gateway` | `:nalo`, `:whatsapp_cloud_api`, and so on | gateway, so two gateways on the same platform (for example two USSD aggregators) do not collide | you are consolidating gateways and want them to share |
103
+ | `:url` | normalized `host + path` | host and path, for per-tenant or per-endpoint isolation (`tenant1.app.com` vs `tenant2.app.com`) | single tenant, single endpoint |
229
104
 
230
- ## Middleware Configuration
105
+ The `:url` segment is `host + path` with the leading slash removed and any character outside `[a-zA-Z0-9._-]` replaced by `_`. If that exceeds 50 characters it becomes the first 41 characters plus an 8-character SHA256 suffix, so it stays bounded but still recognizable.
231
106
 
232
- ### Built-in Middleware
107
+ ### The identifier
233
108
 
234
- ```ruby
235
- # Pagination (USSD only, automatic)
236
- FlowChat::Ussd::Middleware::Pagination
109
+ The identifier is chosen independently of the boundaries and always comes last. Its type is the `identifier` option, or a platform default when unset: WhatsApp uses `:msisdn`, every other platform uses `:request_id`.
237
110
 
238
- # Session management (automatic)
239
- FlowChat::Session::Middleware
111
+ | Type | Value | Hashed |
112
+ |---|---|---|
113
+ | `:request_id` | `context["request.id"]` | Never. It is already opaque. On USSD it is the telco session id, which rotates on timeout, so the session is ephemeral. |
114
+ | `:msisdn` | `context["request.msisdn"]` | Yes, when `hash_identifiers` is true (the default): the first 32 hex characters of SHA256 (128 bits). |
115
+ | `:user_id` | `context["request.user_id"]` | Yes, when `hash_identifiers` is true. |
240
116
 
241
- # Gateway communication (automatic)
242
- FlowChat::Ussd::Gateway::Nalo
243
- FlowChat::Whatsapp::Gateway::CloudApi
244
- ```
117
+ So with the default `hash_identifiers: true`, a phone number never appears in the session key in the clear, while a `request_id` passes through untouched.
245
118
 
246
- ### Custom Middleware
119
+ ### Worked examples
247
120
 
248
- ```ruby
249
- class LoggingMiddleware
250
- def initialize(app)
251
- @app = app
252
- end
253
-
254
- def call(context)
255
- Rails.logger.info "Processing request: #{context.input}"
256
- result = @app.call(context)
257
- Rails.logger.info "Response: #{result[1]}"
258
- result
259
- end
260
- end
121
+ Default config, USSD, `RegistrationFlow`, no durable sessions. The identifier is `:request_id`, the telco session id, so the session lasts only as long as that telco session:
261
122
 
262
- # Use custom middleware
263
- config.use_middleware LoggingMiddleware
123
+ ```
124
+ registration_flow:ussd:nalo:1699_telco_session_42
264
125
  ```
265
126
 
266
- ## Validation Configuration
267
-
268
- ### Error Display Options
127
+ The same, but with `use_durable_sessions` (identifier `:user_id`, which Nalo sets equal to the msisdn) and hashing on. Now the user can time out and dial back into the same session:
269
128
 
270
- ```ruby
271
- # Combine validation error with original message (default)
272
- FlowChat::Config.combine_validation_error_with_message = true
273
- # User sees: "Invalid email format\n\nEnter your email:"
274
-
275
- # Show only validation error
276
- FlowChat::Config.combine_validation_error_with_message = false
277
- # User sees: "Invalid email format"
129
+ ```
130
+ registration_flow:ussd:nalo:9f86d0818...
278
131
  ```
279
132
 
280
- ## Background Job Configuration
133
+ ### Configuring sessions per processor
281
134
 
282
- ### Job Class Setup
135
+ Set boundaries, identifier, and hashing directly:
283
136
 
284
137
  ```ruby
285
- # app/jobs/whatsapp_message_job.rb
286
- class WhatsappMessageJob < ApplicationJob
287
- include FlowChat::Whatsapp::SendJobSupport
288
-
289
- def perform(send_data)
290
- perform_whatsapp_send(send_data)
291
- end
138
+ processor = FlowChat::Processor.new(self) do |config|
139
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
140
+ config.use_session_store FlowChat::Session::CacheSessionStore
141
+ config.use_session_config(boundaries: [:flow, :platform], identifier: :msisdn, hash_identifiers: true)
292
142
  end
293
143
  ```
294
144
 
295
- **Configuration Resolution:** The job automatically resolves configurations using:
296
- 1. Named configuration from `send_data[:configuration_name]` if present
297
- 2. Default configuration from credentials/environment variables
298
-
299
- For custom resolution logic, override the configuration resolution:
145
+ Or take full control of the id with a block:
300
146
 
301
147
  ```ruby
302
- class CustomWhatsappMessageJob < ApplicationJob
303
- include FlowChat::Whatsapp::SendJobSupport
304
-
305
- def perform(send_data)
306
- perform_whatsapp_send(send_data)
307
- end
308
-
309
- private
310
-
311
- def resolve_whatsapp_configuration(send_data)
312
- # Custom logic to resolve configuration
313
- tenant_id = ...
314
- FlowChat::Whatsapp::Configuration.get("tenant_#{tenant_id}")
315
- end
148
+ config.use_session_config do |context|
149
+ "tenant:#{context["request.tenant_id"]}:#{context["request.msisdn"]}"
316
150
  end
317
151
  ```
318
152
 
319
- ### Queue Configuration
153
+ Three convenience methods wrap common changes:
320
154
 
321
- ```ruby
322
- # config/application.rb
323
- config.active_job.queue_adapter = :sidekiq
155
+ | Method | Effect |
156
+ |---|---|
157
+ | `use_durable_sessions` | Sets the identifier to `:user_id`, a stable per-user id. It changes only the identifier, not the boundaries. It matters most on USSD, where `:request_id` rotates on timeout but `:user_id` (which Nalo sets equal to the msisdn) is stable, so the conversation survives a rotation. |
158
+ | `use_cross_platform_sessions` | Narrows boundaries to `[:flow]`, dropping platform and gateway, so one user shares a single session across platforms. |
159
+ | `use_url_isolation` | Appends `:url` to the current boundaries for per-tenant or per-host isolation. |
324
160
 
325
- # config/initializers/flowchat.rb
326
- FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
161
+ ```ruby
162
+ processor = FlowChat::Processor.new(self) do |config|
163
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
164
+ config.use_session_store FlowChat::Session::CacheSessionStore
165
+ config.use_durable_sessions
166
+ end
327
167
  ```
328
168
 
329
- ## Instrumentation Configuration
330
-
331
- ### Basic Setup
169
+ One subtlety with `use_cross_platform_sessions`: dropping the `:platform` boundary only shares a session if the identifier resolves to the same value on both platforms. The platform default identifier differs (WhatsApp uses `:msisdn`, USSD uses `:request_id`), so for real cross-platform sharing pair it with a stable identifier, either `use_durable_sessions` (`:user_id`) or `identifier: :msisdn`.
332
170
 
333
- ```ruby
334
- # Enable instrumentation
335
- FlowChat.setup_instrumentation!
336
- ```
171
+ ## Async processing
337
172
 
338
- ### Custom Event Subscribers
173
+ Enable background processing with `use_async`. It has two forms:
339
174
 
340
175
  ```ruby
341
- # Subscribe to specific events
342
- ActiveSupport::Notifications.subscribe("flow.execution.end.flow_chat") do |event|
343
- # Custom handling
344
- ExternalMonitoring.track_flow_execution(
345
- event.payload[:flow_name],
346
- event.duration
347
- )
348
- end
176
+ # Use a factory (no custom job class needed). The factory: keyword is required.
177
+ config.use_async(factory: :whatsapp)
349
178
 
350
- # Subscribe to all FlowChat events
351
- ActiveSupport::Notifications.subscribe(/\.flow_chat$/) do |name, start, finish, id, payload|
352
- CustomLogger.log_event(name, payload.merge(duration: finish - start))
353
- end
179
+ # Or use your own ActiveJob subclass, with optional job params.
180
+ config.use_async(MyFlowJob, deployment_id: 123)
354
181
  ```
355
182
 
356
- ## Configuration Validation
357
-
358
- FlowChat validates configuration at runtime and provides helpful error messages:
359
-
360
- FlowChat validates configuration at runtime and provides helpful error messages for missing or invalid configurations.
183
+ The webhook enqueues the job and returns immediately; the job re-runs the flow in the background. USSD does not support async, since its protocol needs a synchronous response. See [factory-pattern.md](factory-pattern.md) and [async-background-processing.md](async-background-processing.md).
@@ -0,0 +1,58 @@
1
+ # Factory pattern
2
+
3
+ A factory is a named block that builds and runs a processor. You register it once, then call it by name from both the webhook and the background job. This keeps the processor configuration in one place, so the two contexts cannot drift apart.
4
+
5
+ Without a factory, a webhook that enqueues a background job needs the same processor setup written twice: once in the controller and once in the job. A factory removes the duplication.
6
+
7
+ ## Registering and executing
8
+
9
+ Register the factory in an initializer:
10
+
11
+ ```ruby
12
+ # config/initializers/flow_chat.rb
13
+ FlowChat::Factory.register(:whatsapp) do |controller|
14
+ processor = FlowChat::Processor.new(controller) do |config|
15
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
16
+ config.use_session_store FlowChat::Session::CacheSessionStore
17
+ config.use_async(factory: :whatsapp)
18
+ end
19
+ processor.run(WhatsAppFlow, :start)
20
+ end
21
+ ```
22
+
23
+ Execute it from the webhook controller:
24
+
25
+ ```ruby
26
+ # app/controllers/whatsapp_controller.rb
27
+ class WhatsappController < ApplicationController
28
+ skip_forgery_protection
29
+
30
+ def webhook
31
+ FlowChat::Factory.execute(:whatsapp, controller: self)
32
+ end
33
+ end
34
+ ```
35
+
36
+ The block receives a controller and returns whatever `processor.run` returns. Executing an unregistered name raises `FlowChat::Factory::FactoryNotFoundError`.
37
+
38
+ ## How it pairs with async
39
+
40
+ Notice the factory references itself: `config.use_async(factory: :whatsapp)`. That closes the loop.
41
+
42
+ 1. The webhook calls `Factory.execute(:whatsapp, controller: self)`.
43
+ 2. The factory builds a processor with `use_async(factory: :whatsapp)`, so the gateway enqueues `FlowChat::GenericAsyncJob` with `factory: :whatsapp` and returns immediately.
44
+ 3. The background job calls `Factory.execute(:whatsapp, controller: background_controller)` again.
45
+ 4. This time the gateway is running in the background, so it processes the flow inline and sends the reply.
46
+
47
+ The same factory builds the processor in both passes, so there is one definition of the gateway, session store, and flow. See [async-background-processing.md](async-background-processing.md) for what happens inside the job.
48
+
49
+ ## Other methods
50
+
51
+ - `FlowChat::Factory.registered?(:whatsapp)` returns whether a name is registered.
52
+ - `FlowChat::Factory.registered_factories` lists the registered names.
53
+ - `FlowChat::Factory.clear!` removes all registrations (useful in tests).
54
+
55
+ ## Related
56
+
57
+ - [Async and background processing](async-background-processing.md)
58
+ - [Configuration](configuration.md#async-processing)