flow_chat 0.9.0 → 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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +43 -0
  3. data/CHANGELOG.md +26 -0
  4. data/README.md +166 -295
  5. data/Rakefile +12 -1
  6. data/SECURITY.md +1 -1
  7. data/docs/architecture.md +52 -479
  8. data/docs/async-background-processing.md +31 -265
  9. data/docs/configuration.md +106 -613
  10. data/docs/factory-pattern.md +27 -324
  11. data/docs/gateway-context-variables.md +140 -143
  12. data/docs/gateway-development.md +86 -650
  13. data/docs/getting-started.md +40 -379
  14. data/docs/instrumentation.md +88 -279
  15. data/docs/platforms/instagram.md +278 -0
  16. data/docs/platforms/messenger.md +205 -0
  17. data/docs/platforms/telegram.md +47 -951
  18. data/docs/platforms/ussd.md +38 -653
  19. data/docs/platforms/whatsapp.md +73 -1321
  20. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  21. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  22. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  23. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  24. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  25. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  26. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  27. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  28. data/docs/testing.md +26 -297
  29. data/examples/http_controller.rb +12 -10
  30. data/examples/intercom_configuration_example.rb +19 -24
  31. data/examples/intercom_controller.rb +8 -20
  32. data/examples/multi_tenant_whatsapp_controller.rb +61 -166
  33. data/examples/simulator_controller.rb +0 -1
  34. data/examples/ussd_controller.rb +86 -158
  35. data/examples/whatsapp_controller.rb +16 -15
  36. data/examples/whatsapp_media_examples.rb +27 -79
  37. data/lib/flow_chat/app.rb +129 -11
  38. data/lib/flow_chat/choice_titles.rb +95 -0
  39. data/lib/flow_chat/config.rb +124 -1
  40. data/lib/flow_chat/delivery_error.rb +9 -0
  41. data/lib/flow_chat/http/configuration_error.rb +9 -0
  42. data/lib/flow_chat/http/gateway/simple.rb +24 -4
  43. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  44. data/lib/flow_chat/input.rb +86 -0
  45. data/lib/flow_chat/instagram/client.rb +32 -0
  46. data/lib/flow_chat/instagram/configuration.rb +147 -0
  47. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  48. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  49. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  50. data/lib/flow_chat/instagram/renderer.rb +23 -0
  51. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  52. data/lib/flow_chat/instrumentation.rb +160 -1
  53. data/lib/flow_chat/intercom/client.rb +34 -28
  54. data/lib/flow_chat/intercom/configuration.rb +2 -49
  55. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  56. data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
  57. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  58. data/lib/flow_chat/intercom/renderer.rb +57 -5
  59. data/lib/flow_chat/media.rb +121 -0
  60. data/lib/flow_chat/messenger/client.rb +264 -0
  61. data/lib/flow_chat/messenger/configuration.rb +103 -0
  62. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  63. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  64. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  65. data/lib/flow_chat/messenger/renderer.rb +150 -0
  66. data/lib/flow_chat/meta/challenge.rb +24 -0
  67. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  68. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  69. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  70. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  71. data/lib/flow_chat/meta/signature.rb +30 -0
  72. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  73. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  74. data/lib/flow_chat/named_configuration.rb +65 -0
  75. data/lib/flow_chat/prompt.rb +13 -16
  76. data/lib/flow_chat/renderers/markdown_support.rb +109 -0
  77. data/lib/flow_chat/security.rb +76 -0
  78. data/lib/flow_chat/session/middleware.rb +11 -2
  79. data/lib/flow_chat/simulator/controller.rb +31 -15
  80. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  81. data/lib/flow_chat/telegram/client.rb +47 -4
  82. data/lib/flow_chat/telegram/configuration.rb +2 -42
  83. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  84. data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
  85. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
  86. data/lib/flow_chat/telegram/renderer.rb +10 -2
  87. data/lib/flow_chat/text_truncator.rb +75 -0
  88. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  89. data/lib/flow_chat/version.rb +1 -1
  90. data/lib/flow_chat/whatsapp/client.rb +14 -7
  91. data/lib/flow_chat/whatsapp/configuration.rb +12 -51
  92. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  93. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +254 -203
  94. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
  95. data/lib/flow_chat/whatsapp/renderer.rb +121 -60
  96. data/lib/flow_chat.rb +0 -10
  97. data/site/.nojekyll +0 -0
  98. data/site/.og-card.html +89 -0
  99. data/site/favicon.svg +6 -0
  100. data/site/index.html +209 -0
  101. data/site/og.png +0 -0
  102. metadata +51 -3
  103. data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
@@ -1,690 +1,183 @@
1
- # FlowChat Configuration
1
+ # Configuration
2
2
 
3
- This guide covers all configuration options for FlowChat, from basic setup to advanced customization for production deployments.
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
- ## Global Configuration
5
+ ## Global configuration
6
6
 
7
- Configure FlowChat globally in `config/initializers/flow_chat.rb`:
7
+ Set these once, for example in `config/initializers/flow_chat.rb`.
8
8
 
9
- ```ruby
10
- # Basic configuration
11
- FlowChat::Config.logger = Rails.logger
12
- FlowChat::Config.cache = Rails.cache
13
-
14
- # Validation behavior
15
- FlowChat::Config.combine_validation_error_with_message = true
16
-
17
- # Simulator settings (for development/testing)
18
- FlowChat::Config.simulator_secret = "your_simulator_secret"
19
- ```
20
-
21
- ## Platform-Specific Configuration
22
-
23
- ### USSD 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. |
24
16
 
25
17
  ```ruby
26
- # USSD pagination settings
27
- FlowChat::Config.ussd.pagination_page_size = 160 # Characters per page
28
- FlowChat::Config.ussd.pagination_next_option = "#" # Next page option
29
- FlowChat::Config.ussd.pagination_back_option = "0" # Previous page option
30
- FlowChat::Config.ussd.pagination_next_text = "More" # Next page text
31
- FlowChat::Config.ussd.pagination_back_text = "Back" # Previous page text
32
- ```
33
-
34
- ### WhatsApp Configuration
35
-
36
- ```ruby
37
- # WhatsApp API configuration
38
- FlowChat::Config.whatsapp.api_base_url = "https://graph.facebook.com/v23.0"
39
- ```
40
-
41
- ### HTTP Configuration
42
-
43
- ```ruby
44
- # HTTP gateway settings
45
- FlowChat::Config.http.default_gateway = :simple
46
- FlowChat::Config.http.request_timeout = 30
47
- FlowChat::Config.http.response_format = :json
48
- ```
49
-
50
- ## Processor Configuration
51
-
52
- Configure processors for each request:
53
-
54
- ```ruby
55
- processor = FlowChat::Processor.new(self) do |config|
56
- # Gateway configuration
57
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
58
-
59
- # Session configuration
60
- config.use_session_store FlowChat::Session::CacheSessionStore
61
- config.use_session_config(
62
- boundaries: [:flow, :platform, :gateway],
63
- identifier: :msisdn,
64
- hash_identifiers: true
65
- )
66
-
67
- # Middleware configuration
68
- config.use_middleware LoggingMiddleware
69
- config.use_middleware AuthenticationMiddleware
70
-
71
- # Convenience methods
72
- config.use_durable_sessions
73
- config.use_cross_platform_sessions
74
- config.use_url_isolation
75
- end
76
- ```
77
-
78
- ## Session Configuration
79
-
80
- ### Session Boundaries
81
-
82
- Control how session IDs are generated:
83
-
84
- ```ruby
85
- # Available boundaries
86
- boundaries: [
87
- :flow, # Separate sessions per flow class
88
- :platform, # Separate sessions per platform (ussd, whatsapp, http)
89
- :gateway, # Separate sessions per gateway (nalo, cloud_api, etc.)
90
- :url # Separate sessions per URL (multi-tenancy)
91
- ]
92
-
93
- # Examples
94
- config.use_session_config(boundaries: [:flow]) # "survey_flow:user123"
95
- config.use_session_config(boundaries: [:flow, :platform]) # "survey_flow:ussd:user123"
96
- config.use_session_config(boundaries: [:flow, :url]) # "survey_flow:tenant1.app.com:user123"
97
- ```
98
-
99
- ### Custom Session ID Generation
100
-
101
- For complete control over session ID format, use a block:
102
-
103
- ```ruby
104
- # Custom session ID with proc/block
105
- config.use_session_config do |context|
106
- # Access full context for custom logic
107
- user_phone = context["request.msisdn"]
108
- flow_name = context["flow.name"]
109
- gateway = context["request.gateway"]
110
- timestamp = Time.current.strftime("%Y%m%d")
111
-
112
- # Return your custom session ID
113
- "#{flow_name}_#{gateway}_#{timestamp}_#{hash_phone(user_phone)}"
114
- end
115
-
116
- # Multi-tenant example
117
- config.use_session_config do |context|
118
- tenant_id = extract_tenant(context)
119
- user_id = context["request.user_id"] || context["request.msisdn"]
120
- "tenant_#{tenant_id}_user_#{user_id.hash.abs}"
121
- end
122
-
123
- # API-based session IDs
124
- config.use_session_config do |context|
125
- api_key = context.controller.request.headers["X-API-Key"]
126
- "api_#{Digest::SHA256.hexdigest(api_key)[0,8]}_#{context['request.id']}"
127
- end
128
- ```
129
-
130
- **Note**: When using a custom proc, it takes precedence over all boundary and identifier settings. The proc receives the full FlowChat context and should return a string session ID.
131
-
132
- ### Session Identifiers
133
-
134
- Choose what identifies a user session:
135
-
136
- ```ruby
137
- # Identifier types
138
- :request_id # Ephemeral (new session each request)
139
- :user_id # Durable using user_id field
140
- :msisdn # Durable using phone number
141
-
142
- # Examples
143
- config.use_session_config(identifier: :request_id) # Default for HTTP
144
- config.use_session_config(identifier: :msisdn) # Default for WhatsApp/USSD
145
- config.use_session_config(identifier: :user_id) # For authenticated users
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
146
22
  ```
147
23
 
148
- ### Session Stores
149
-
150
- Choose where session data is stored:
151
-
152
- ```ruby
153
- # Built-in session stores
154
- config.use_session_store FlowChat::Session::CacheSessionStore # Rails sessions
155
- config.use_session_store FlowChat::Session::CacheSessionStore # Rails cache
24
+ ## USSD configuration
156
25
 
157
- # Custom session store
158
- config.use_session_store MyCompany::CustomSessionStore
159
- ```
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.
160
27
 
161
- ### Convenience Methods
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. |
162
35
 
163
36
  ```ruby
164
- # Durable sessions (use phone number, survive timeouts)
165
- config.use_durable_sessions
166
-
167
- # Cross-platform sessions (same user across USSD/WhatsApp)
168
- config.use_cross_platform_sessions
169
-
170
- # URL-based isolation (multi-tenancy)
171
- config.use_url_isolation
37
+ FlowChat::Config.ussd.pagination_page_size = 160
172
38
  ```
173
39
 
174
- ## Gateway Configuration
40
+ ## WhatsApp configuration
175
41
 
176
- ### Built-in Gateways
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).
177
43
 
178
- #### USSD - Nalo Gateway
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. |
179
47
 
180
- ```ruby
181
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
48
+ ## HTTP configuration
182
49
 
183
- # No additional configuration required for Nalo
184
- ```
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.
185
51
 
186
- #### WhatsApp - Cloud API Gateway
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. |
187
57
 
188
- ```ruby
189
- # Basic configuration (uses Rails credentials)
190
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
191
-
192
- # Custom configuration
193
- whatsapp_config = FlowChat::Whatsapp::Configuration.new
194
- whatsapp_config.access_token = ENV["WHATSAPP_ACCESS_TOKEN"]
195
- whatsapp_config.phone_number_id = ENV["WHATSAPP_PHONE_NUMBER_ID"]
196
- whatsapp_config.verify_token = ENV["WHATSAPP_VERIFY_TOKEN"]
197
- whatsapp_config.app_secret = ENV["WHATSAPP_APP_SECRET"]
198
- whatsapp_config.skip_signature_validation = Rails.env.development?
199
-
200
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
201
- ```
58
+ ## Sessions
202
59
 
203
- #### HTTP - Simple Gateway
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.
204
61
 
205
- ```ruby
206
- # HTTP gateway requires user_params hash
207
- config.use_gateway FlowChat::Http::Gateway::Simple, {
208
- session_id: "unique_session_id", # required
209
- user_id: "user_identifier", # required
210
- name: "John Doe", # optional
211
- msisdn: "+256700123456", # optional
212
- email: "user@example.com" # optional
213
- }
214
- ```
62
+ `FlowChat::Config.session` sets the defaults.
215
63
 
216
- **Required Parameters:**
217
- - `session_id` - Unique identifier for the session
218
- - `user_id` - User identifier
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. |
219
70
 
220
- **Optional Parameters:**
221
- - `name` - User's display name (sets `request.user_name`)
222
- - `msisdn` - E.164 formatted phone number (sets `request.msisdn`)
223
- - `email` - User email address (sets `request.email`)
71
+ ### How the session id is built
224
72
 
225
- **Note:** User input comes from `params["input"]` in the request. The `message_id` is auto-generated as a UUID.
73
+ FlowChat sets the session id one of three ways, in order of precedence:
226
74
 
227
- ### Custom Gateway Configuration
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.
228
78
 
229
- ```ruby
230
- # Custom gateway with configuration
231
- my_config = MyCompany::CustomGatewayConfig.new
232
- my_config.api_key = ENV["CUSTOM_API_KEY"]
233
- my_config.endpoint = ENV["CUSTOM_ENDPOINT"]
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:
234
80
 
235
- config.use_gateway MyCompany::CustomGateway, my_config
236
81
  ```
237
-
238
- ## Environment-Specific Configuration
239
-
240
- ### Development Configuration
241
-
242
- ```ruby
243
- # config/environments/development.rb
244
- Rails.application.configure do
245
- # Enable detailed logging
246
- config.log_level = :debug
247
-
248
- # FlowChat development settings
249
- config.after_initialize do
250
- FlowChat::Config.logger = Rails.logger
251
- FlowChat::Config.logger.level = Logger::DEBUG
252
-
253
- # USSD settings for development
254
- FlowChat::Config.ussd.pagination_page_size = 200 # Larger for easier testing
255
-
256
- # WhatsApp settings for development
257
- FlowChat::Config.whatsapp.message_handling_mode = :inline
258
-
259
- # Enable simulator
260
- FlowChat::Config.simulator_secret = "dev_secret_123"
261
- end
262
- end
82
+ flow_name : platform : gateway : url : identifier
263
83
  ```
264
84
 
265
- ### Production Configuration
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:
266
86
 
267
- ```ruby
268
- # config/environments/production.rb
269
- Rails.application.configure do
270
- # Production logging
271
- config.log_level = :info
272
-
273
- # FlowChat production settings
274
- config.after_initialize do
275
- FlowChat::Config.logger = Rails.logger
276
-
277
- # USSD production settings
278
- FlowChat::Config.ussd.pagination_page_size = 140 # Conservative for compatibility
279
-
280
- # WhatsApp uses inline by default
281
-
282
- # Disable simulator in production
283
- FlowChat::Config.simulator_secret = nil
284
- end
285
- end
286
87
  ```
287
-
288
- ### Staging Configuration
289
-
290
- ```ruby
291
- # config/environments/staging.rb
292
- Rails.application.configure do
293
- # Staging-specific settings
294
- config.after_initialize do
295
- FlowChat::Config.logger = Rails.logger
296
- FlowChat::Config.logger.level = Logger::DEBUG # More verbose for staging
297
-
298
- # Use background processing but with shorter delays
299
- FlowChat::Config.whatsapp.message_handling_mode = :background
300
-
301
- # Enable simulator with staging secret
302
- FlowChat::Config.simulator_secret = ENV["STAGING_SIMULATOR_SECRET"]
303
- end
304
- end
88
+ registration_flow:whatsapp:whatsapp_cloud_api:9f86d0818...
89
+ (flow) (platform) (gateway) (hashed msisdn)
305
90
  ```
306
91
 
307
- ## Multi-Tenant Configuration
92
+ The hashed identifier is shown truncated here for readability; the real key carries the 32-character (128-bit) SHA256 prefix.
308
93
 
309
- ### URL-Based Tenancy
94
+ ### What each boundary isolates
310
95
 
311
- ```ruby
312
- class MultiTenantController < ApplicationController
313
- def process_request
314
- processor = FlowChat::Processor.new(self) do |config|
315
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, tenant_whatsapp_config
316
- config.use_session_store FlowChat::Session::CacheSessionStore
317
-
318
- # Enable URL-based session isolation
319
- config.use_url_isolation
320
-
321
- # Optional: Additional tenant isolation
322
- config.use_session_config(
323
- boundaries: [:flow, :platform, :url],
324
- identifier: :user_id
325
- )
326
- end
327
-
328
- processor.run tenant_flow_class, :main_action
329
- end
330
-
331
- private
332
-
333
- def tenant_whatsapp_config
334
- tenant = extract_tenant_from_request
335
- tenant.whatsapp_configuration
336
- end
337
-
338
- def tenant_flow_class
339
- tenant = extract_tenant_from_request
340
- tenant.flow_class
341
- end
342
-
343
- def extract_tenant_from_request
344
- # Extract tenant from subdomain, domain, or path
345
- subdomain = request.subdomain
346
- Tenant.find_by(subdomain: subdomain)
347
- end
348
- end
349
- ```
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.
350
97
 
351
- ### Database-Based Tenancy
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 |
352
104
 
353
- ```ruby
354
- class TenantSpecificController < ApplicationController
355
- before_action :set_tenant
356
-
357
- def process_request
358
- processor = FlowChat::Processor.new(self) do |config|
359
- config.use_gateway gateway_for_tenant
360
- config.use_session_store FlowChat::Session::CacheSessionStore
361
-
362
- # Custom session boundaries for tenant isolation
363
- config.use_session_config(
364
- boundaries: [:flow, :platform],
365
- identifier: :user_id
366
- )
367
- end
368
-
369
- processor.run @tenant.flow_class.constantize, :main_action
370
- end
371
-
372
- private
373
-
374
- def set_tenant
375
- @tenant = Tenant.find(params[:tenant_id])
376
- end
377
-
378
- def gateway_for_tenant
379
- case @tenant.platform
380
- when 'whatsapp'
381
- FlowChat::Whatsapp::Gateway::CloudApi
382
- when 'ussd'
383
- FlowChat::Ussd::Gateway::Nalo
384
- else
385
- FlowChat::Http::Gateway::Simple
386
- end
387
- end
388
- end
389
- ```
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.
390
106
 
391
- ## Security Configuration
107
+ ### The identifier
392
108
 
393
- ### WhatsApp Signature Validation
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`.
394
110
 
395
- ```ruby
396
- # Always validate signatures in production
397
- whatsapp_config = FlowChat::Whatsapp::Configuration.new
398
- whatsapp_config.app_secret = ENV["WHATSAPP_APP_SECRET"]
399
- whatsapp_config.skip_signature_validation = false # Never skip in production
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. |
400
116
 
401
- # Only skip validation in development if needed
402
- whatsapp_config.skip_signature_validation = Rails.env.development?
403
- ```
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.
404
118
 
405
- ### Session Security
119
+ ### Worked examples
406
120
 
407
- ```ruby
408
- # Hash sensitive identifiers
409
- config.use_session_config(
410
- identifier: :msisdn,
411
- hash_identifiers: true # Phone numbers are hashed for privacy
412
- )
413
-
414
- # Implement custom session store with encryption
415
- class EncryptedSessionStore < FlowChat::Session::CacheSessionStore
416
- def set(key, value)
417
- encrypted_value = encrypt(value.to_json)
418
- super(key, encrypted_value)
419
- end
420
-
421
- def get(key)
422
- encrypted_value = super(key)
423
- return nil unless encrypted_value
424
-
425
- decrypted = decrypt(encrypted_value)
426
- JSON.parse(decrypted)
427
- rescue
428
- nil # Return nil if decryption fails
429
- end
430
-
431
- private
432
-
433
- def encrypt(data)
434
- # Use Rails credentials or environment variable
435
- secret = Rails.application.credentials.session_encryption_key
436
- crypt = ActiveSupport::MessageEncryptor.new(secret)
437
- crypt.encrypt_and_sign(data)
438
- end
439
-
440
- def decrypt(encrypted_data)
441
- secret = Rails.application.credentials.session_encryption_key
442
- crypt = ActiveSupport::MessageEncryptor.new(secret)
443
- crypt.decrypt_and_verify(encrypted_data)
444
- end
445
- 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:
446
122
 
447
- # Use encrypted session store
448
- config.use_session_store EncryptedSessionStore
449
123
  ```
450
-
451
- ## Performance Configuration
452
-
453
- ### Redis Cache Configuration
454
-
455
- ```ruby
456
- # config/initializers/redis.rb
457
- redis_config = {
458
- url: ENV["REDIS_URL"],
459
- pool_size: ENV.fetch("REDIS_POOL_SIZE", 5).to_i,
460
- pool_timeout: ENV.fetch("REDIS_POOL_TIMEOUT", 5).to_i
461
- }
462
-
463
- # Use Redis for session storage
464
- Rails.application.configure do
465
- config.cache_store = :redis_cache_store, redis_config
466
- end
467
-
468
- # FlowChat will automatically use Rails.cache
469
- FlowChat::Config.cache = Rails.cache
124
+ registration_flow:ussd:nalo:1699_telco_session_42
470
125
  ```
471
126
 
472
- ### Background Job Configuration
473
-
474
- ```ruby
475
- # config/initializers/sidekiq.rb (if using Sidekiq)
476
- Sidekiq.configure_server do |config|
477
- config.redis = { url: ENV["REDIS_URL"] }
478
- end
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:
479
128
 
480
- Sidekiq.configure_client do |config|
481
- config.redis = { url: ENV["REDIS_URL"] }
482
- end
483
-
484
- # WhatsApp uses inline responses by defaul
485
129
  ```
486
-
487
- ### Database Connection Configuration
488
-
489
- ```ruby
490
- # config/database.yml
491
- production:
492
- adapter: postgresql
493
- pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
494
- timeout: 5000
495
- # ... other database settings
496
-
497
- # For high-volume USSD applications
498
- checkout_timeout: 2
499
- reaping_frequency: 10
130
+ registration_flow:ussd:nalo:9f86d0818...
500
131
  ```
501
132
 
502
- ## Monitoring Configuration
133
+ ### Configuring sessions per processor
503
134
 
504
- ### Instrumentation Setup
135
+ Set boundaries, identifier, and hashing directly:
505
136
 
506
137
  ```ruby
507
- # config/initializers/flow_chat.rb
508
- FlowChat.setup_instrumentation!
509
-
510
- # Subscribe to FlowChat events
511
- ActiveSupport::Notifications.subscribe("flow_chat.message_received") do |event|
512
- # Log message received
513
- Rails.logger.info "Message received: #{event.payload}"
514
- end
515
-
516
- ActiveSupport::Notifications.subscribe("flow_chat.message_sent") do |event|
517
- # Log message sent
518
- Rails.logger.info "Message sent: #{event.payload}"
519
- end
520
-
521
- ActiveSupport::Notifications.subscribe("flow_chat.flow_execution_error") do |event|
522
- # Alert on flow errors
523
- ErrorNotificationService.notify(event.payload)
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)
524
142
  end
525
143
  ```
526
144
 
527
- ### Custom Metrics
145
+ Or take full control of the id with a block:
528
146
 
529
147
  ```ruby
530
- # Custom metrics collector
531
- class CustomMetricsCollector
532
- def self.collect_metrics
533
- {
534
- active_sessions: count_active_sessions,
535
- messages_per_minute: calculate_message_rate,
536
- error_rate: calculate_error_rate
537
- }
538
- end
539
-
540
- private
541
-
542
- def self.count_active_sessions
543
- # Implementation depends on your session store
544
- Rails.cache.redis.keys("flow_chat:session:*").count
545
- end
546
-
547
- def self.calculate_message_rate
548
- # Implementation depends on your metrics storage
549
- # Return messages processed in the last minute
550
- end
551
-
552
- def self.calculate_error_rate
553
- # Calculate error percentage
554
- end
148
+ config.use_session_config do |context|
149
+ "tenant:#{context["request.tenant_id"]}:#{context["request.msisdn"]}"
555
150
  end
556
-
557
- # Setup metrics collection
558
- FlowChat::Config.metrics_collector = CustomMetricsCollector
559
151
  ```
560
152
 
561
- ## Testing Configuration
153
+ Three convenience methods wrap common changes:
562
154
 
563
- ### Test Environment
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. |
564
160
 
565
161
  ```ruby
566
- # config/environments/test.rb
567
- Rails.application.configure do
568
- config.after_initialize do
569
- FlowChat::Config.logger = Rails.logger
570
- FlowChat::Config.logger.level = Logger::ERROR # Reduce noise in tests
571
-
572
- # Use memory cache for tests
573
- FlowChat::Config.cache = ActiveSupport::Cache::MemoryStore.new
574
-
575
- # Disable background processing in tests
576
- FlowChat::Config.whatsapp.message_handling_mode = :inline
577
-
578
- # Enable simulator for testing
579
- FlowChat::Config.simulator_secret = "test_secret"
580
- end
581
- end
582
- ```
583
-
584
- ### Test Helper Configuration
585
-
586
- ```ruby
587
- # test/test_helper.rb
588
- class ActiveSupport::TestCase
589
- def setup_flow_chat_test_environment
590
- # Reset FlowChat state between tests
591
- FlowChat::Config.cache.clear
592
-
593
- # Mock external API calls
594
- stub_whatsapp_api_calls
595
- stub_ussd_gateway_calls
596
- end
597
-
598
- def create_test_processor(platform: :ussd, **options)
599
- gateway_class = case platform
600
- when :ussd then FlowChat::Ussd::Gateway::Nalo
601
- when :whatsapp then FlowChat::Whatsapp::Gateway::CloudApi
602
- when :http then FlowChat::Http::Gateway::Simple
603
- end
604
-
605
- FlowChat::Processor.new(MockController.new) do |config|
606
- config.use_gateway gateway_class
607
- config.use_session_store FlowChat::Session::MemorySessionStore
608
- options.each { |key, value| config.send(key, value) }
609
- end
610
- end
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
611
166
  end
612
167
  ```
613
168
 
614
- ## Troubleshooting Configuration
615
-
616
- ### Debug Mode
617
-
618
- ```ruby
619
- # Enable comprehensive debugging
620
- FlowChat::Config.logger.level = Logger::DEBUG
621
-
622
- # Add custom debug middleware
623
- class DebugMiddleware
624
- def initialize(app)
625
- @app = app
626
- end
627
-
628
- def call(context)
629
- Rails.logger.debug "=== FlowChat Debug ==="
630
- Rails.logger.debug "Context: #{context.to_h}"
631
- Rails.logger.debug "Input: #{context.input.inspect}"
632
- Rails.logger.debug "Session ID: #{context['session.id']}"
633
-
634
- result = @app.call(context)
635
-
636
- Rails.logger.debug "Result: #{result.inspect}"
637
- Rails.logger.debug "======================"
638
-
639
- result
640
- end
641
- end
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`.
642
170
 
643
- # Use debug middleware
644
- config.use_middleware DebugMiddleware
645
- ```
171
+ ## Async processing
646
172
 
647
- ### Configuration Validation
173
+ Enable background processing with `use_async`. It has two forms:
648
174
 
649
175
  ```ruby
650
- # Add configuration validation
651
- class ConfigurationValidator
652
- def self.validate!
653
- validate_environment_variables!
654
- validate_gateway_configuration!
655
- validate_session_configuration!
656
- end
657
-
658
- private
659
-
660
- def self.validate_environment_variables!
661
- required_vars = %w[
662
- WHATSAPP_ACCESS_TOKEN
663
- WHATSAPP_PHONE_NUMBER_ID
664
- WHATSAPP_VERIFY_TOKEN
665
- ]
666
-
667
- missing = required_vars.select { |var| ENV[var].blank? }
668
- raise "Missing environment variables: #{missing.join(', ')}" if missing.any?
669
- end
670
-
671
- def self.validate_gateway_configuration!
672
- # Validate gateway-specific configuration
673
- end
674
-
675
- def self.validate_session_configuration!
676
- # Validate session store connectivity
677
- FlowChat::Config.cache.write("test_key", "test_value")
678
- FlowChat::Config.cache.delete("test_key")
679
- rescue => e
680
- raise "Session store configuration invalid: #{e.message}"
681
- end
682
- end
176
+ # Use a factory (no custom job class needed). The factory: keyword is required.
177
+ config.use_async(factory: :whatsapp)
683
178
 
684
- # Run validation on startup
685
- Rails.application.config.after_initialize do
686
- ConfigurationValidator.validate! if Rails.env.production?
687
- end
179
+ # Or use your own ActiveJob subclass, with optional job params.
180
+ config.use_async(MyFlowJob, deployment_id: 123)
688
181
  ```
689
182
 
690
- This configuration guide covers all aspects of FlowChat setup. For platform-specific configuration details, see the individual platform guides.
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).