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.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +43 -0
- data/CHANGELOG.md +26 -0
- data/README.md +166 -295
- data/Rakefile +12 -1
- data/SECURITY.md +1 -1
- data/docs/architecture.md +52 -479
- data/docs/async-background-processing.md +31 -265
- data/docs/configuration.md +106 -613
- data/docs/factory-pattern.md +27 -324
- data/docs/gateway-context-variables.md +140 -143
- data/docs/gateway-development.md +86 -650
- data/docs/getting-started.md +40 -379
- data/docs/instrumentation.md +88 -279
- data/docs/platforms/instagram.md +278 -0
- data/docs/platforms/messenger.md +205 -0
- data/docs/platforms/telegram.md +47 -951
- data/docs/platforms/ussd.md +38 -653
- data/docs/platforms/whatsapp.md +73 -1321
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
- data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
- data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
- data/docs/testing.md +26 -297
- data/examples/http_controller.rb +12 -10
- data/examples/intercom_configuration_example.rb +19 -24
- data/examples/intercom_controller.rb +8 -20
- data/examples/multi_tenant_whatsapp_controller.rb +61 -166
- data/examples/simulator_controller.rb +0 -1
- data/examples/ussd_controller.rb +86 -158
- data/examples/whatsapp_controller.rb +16 -15
- data/examples/whatsapp_media_examples.rb +27 -79
- data/lib/flow_chat/app.rb +129 -11
- data/lib/flow_chat/choice_titles.rb +95 -0
- data/lib/flow_chat/config.rb +124 -1
- data/lib/flow_chat/delivery_error.rb +9 -0
- data/lib/flow_chat/http/configuration_error.rb +9 -0
- data/lib/flow_chat/http/gateway/simple.rb +24 -4
- data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
- data/lib/flow_chat/input.rb +86 -0
- data/lib/flow_chat/instagram/client.rb +32 -0
- data/lib/flow_chat/instagram/configuration.rb +147 -0
- data/lib/flow_chat/instagram/configuration_error.rb +7 -0
- data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
- data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
- data/lib/flow_chat/instagram/renderer.rb +23 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
- data/lib/flow_chat/instrumentation.rb +160 -1
- data/lib/flow_chat/intercom/client.rb +34 -28
- data/lib/flow_chat/intercom/configuration.rb +2 -49
- data/lib/flow_chat/intercom/configuration_error.rb +9 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
- data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
- data/lib/flow_chat/intercom/renderer.rb +57 -5
- data/lib/flow_chat/media.rb +121 -0
- data/lib/flow_chat/messenger/client.rb +264 -0
- data/lib/flow_chat/messenger/configuration.rb +103 -0
- data/lib/flow_chat/messenger/configuration_error.rb +9 -0
- data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
- data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
- data/lib/flow_chat/messenger/renderer.rb +150 -0
- data/lib/flow_chat/meta/challenge.rb +24 -0
- data/lib/flow_chat/meta/choice_ladder.rb +37 -0
- data/lib/flow_chat/meta/configuration_error.rb +7 -0
- data/lib/flow_chat/meta/gateway_identity.rb +38 -0
- data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
- data/lib/flow_chat/meta/signature.rb +30 -0
- data/lib/flow_chat/meta/signature_validation.rb +66 -0
- data/lib/flow_chat/meta/webhook_verification.rb +43 -0
- data/lib/flow_chat/named_configuration.rb +65 -0
- data/lib/flow_chat/prompt.rb +13 -16
- data/lib/flow_chat/renderers/markdown_support.rb +109 -0
- data/lib/flow_chat/security.rb +76 -0
- data/lib/flow_chat/session/middleware.rb +11 -2
- data/lib/flow_chat/simulator/controller.rb +31 -15
- data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
- data/lib/flow_chat/telegram/client.rb +47 -4
- data/lib/flow_chat/telegram/configuration.rb +2 -42
- data/lib/flow_chat/telegram/configuration_error.rb +9 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
- data/lib/flow_chat/telegram/renderer.rb +10 -2
- data/lib/flow_chat/text_truncator.rb +75 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +14 -7
- data/lib/flow_chat/whatsapp/configuration.rb +12 -51
- data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +254 -203
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
- data/lib/flow_chat/whatsapp/renderer.rb +121 -60
- data/lib/flow_chat.rb +0 -10
- data/site/.nojekyll +0 -0
- data/site/.og-card.html +89 -0
- data/site/favicon.svg +6 -0
- data/site/index.html +209 -0
- data/site/og.png +0 -0
- metadata +51 -3
- data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
data/docs/configuration.md
CHANGED
|
@@ -1,690 +1,183 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Configuration
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
5
|
+
## Global configuration
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Set these once, for example in `config/initializers/flow_chat.rb`.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
FlowChat::Config.logger
|
|
12
|
-
FlowChat::Config.cache
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
FlowChat::Config.
|
|
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
|
-
#
|
|
27
|
-
FlowChat::Config.
|
|
28
|
-
FlowChat::Config.
|
|
29
|
-
FlowChat::Config.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
40
|
+
## WhatsApp configuration
|
|
175
41
|
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
48
|
+
## HTTP configuration
|
|
182
49
|
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
FlowChat sets the session id one of three ways, in order of precedence:
|
|
226
74
|
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
The hashed identifier is shown truncated here for readability; the real key carries the 32-character (128-bit) SHA256 prefix.
|
|
308
93
|
|
|
309
|
-
###
|
|
94
|
+
### What each boundary isolates
|
|
310
95
|
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
### The identifier
|
|
392
108
|
|
|
393
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
119
|
+
### Worked examples
|
|
406
120
|
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
### Configuring sessions per processor
|
|
503
134
|
|
|
504
|
-
|
|
135
|
+
Set boundaries, identifier, and hashing directly:
|
|
505
136
|
|
|
506
137
|
```ruby
|
|
507
|
-
|
|
508
|
-
FlowChat
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
145
|
+
Or take full control of the id with a block:
|
|
528
146
|
|
|
529
147
|
```ruby
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
153
|
+
Three convenience methods wrap common changes:
|
|
562
154
|
|
|
563
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
config.
|
|
569
|
-
|
|
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
|
-
|
|
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
|
-
|
|
644
|
-
config.use_middleware DebugMiddleware
|
|
645
|
-
```
|
|
171
|
+
## Async processing
|
|
646
172
|
|
|
647
|
-
|
|
173
|
+
Enable background processing with `use_async`. It has two forms:
|
|
648
174
|
|
|
649
175
|
```ruby
|
|
650
|
-
#
|
|
651
|
-
|
|
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
|
-
#
|
|
685
|
-
|
|
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
|
-
|
|
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).
|