Chatwoot Infrastructure
Pulumi project that declares the configuration running on the self-hosted Chatwoot at support.studyflash.ch (account_id 2).
Chatwoot Infrastructure
Pulumi project that declares the configuration running on the
self-hosted Chatwoot at support.studyflash.ch (account_id 2).
Automation rules
assign-dev-support-to-rajiv— assigns conversations labelleddev-supportto Rajiv (currently inactive).rxdb-bug-on-create— auto-replies onconversation_createdwhen the contact email containsRxDB Error-Code: DB6.rxdb-bug-on-open— auto-replies onconversation_openedwhen the contact email containsRxDB Error-Code.
Agent bots
maximilian— the support bot's Chatwoot identity. Declared but not bound to any inbox. Chatwoot bot-scope tokens are too narrow for the support-bot's needs (no labels, no reads), so we kept the legacyn8n-legacywebhook as the live event path. Maximilian remains as reproducible config and itsaccessTokenis exported as a stack output — useful for future signature verification on the webhook receiver, or for bot-attributed POSTs (the operations the bot scope does allow: messages, custom_attributes, toggle_status, assignments, sla_policy).
Inboxes
support-mailbox(id1) — Email channel, Microsoft 365 OAuth. Imap/SMTP credentials are not modeled in the resource (deliberately narrow schema) and stay whatever Chatwoot has on disk.mobile-app-api(id5) — API channel;webhook_urlpoints at the productionstudyflash-core-apiworker. (Was previously pointing at a stale PR-1959 preview URL — fixed by this PR.)
Webhooks
n8n-legacy(id1, Chatwoot nameN8N) — historical account-level webhook deliveringmessage_createdevents to the support-bot. Brought under management here so it can be torn down declaratively in a future change once we have a replacement event path.
SLA assignment for premium users is handled separately by the
support-bot calling Chatwoot's
apply_sla_policy API directly — there is no automation rule for it.
How this is wired
There's no Pulumi-shipped or community Terraform provider for Chatwoot,
so we ship our own at provider/ — a Go-native plugin
built on pulumi-go-provider.
This stack consumes the generated TypeScript SDK
(studyflash-chatwoot-provider) via workspace:*. See
provider/README.md for the resource catalogue,
the build/regenerate workflow, and the full list of payload-envelope
quirks the client transparently unwraps.
The provider used to live as a Pulumi Dynamic Provider directly inside
this directory (automationRule.ts, agentBot.ts, inboxAgentBot.ts).
The dynamic-provider closure was serialised into each resource's state,
so any provider edit triggered a replace that called delete on every
live Chatwoot resource — catastrophic for production. The Go-native
rewrite has none of those gaps: read works, pulumi import works, and
provider edits are a no-op on existing state.
Stack outputs
| Name | Type | Notes |
|---|---|---|
maximilianAccessToken | secret string | The bot's Chatwoot access_token. Currently unused by any consumer. Available for future use (webhook signature verification, bot-attributed POSTs) via pulumi.StackReference. |
Apply
Run the shared Pulumi bootstrap once per machine (handles pulumi login
- R2 credentials — see
infra/scripts/), then:
pnpm preview # pulumi preview --diff
pnpm run pulumi:up # pulumi up
Both wrap infisical run --path=/internal/chatwoot/ -- pulumi <action>
so the Chatwoot creds reach the provider without ever touching disk.
No cross-stack dependency is live today. maximilianAccessToken is
exported but unconsumed — feel free to apply this stack on its own.
Credentials
Stack-specific creds are sourced from Infisical at /internal/chatwoot/
on the prod env. (Backend creds — PULUMI_BACKEND_URL, R2 keys — are
shared across all stacks and live at /infra/scripts/; the bootstrap
script handles them.)
| Var | Where it comes from | Why it's needed |
|---|---|---|
CHATWOOT_API_URL | Infisical (https://support.studyflash.ch) | Base URL for the REST calls |
CHATWOOT_API_KEY | Infisical (Chatwoot user api_access_token) | api_access_token header |
The Chatwoot API key should belong to a dedicated automation user with
admin scope on account 2. Don't reuse a personal token — the audit log
will attribute every Pulumi-driven change to whoever's token it is.
Stack config
config:
studyflash-chatwoot:accountId: "2"
accountId defaults to "2" in index.ts if unset, so a one-off override
per stack is possible without code changes.
State migration from the dynamic provider
Existing prod state holds the three rules + maximilian + the inbox
binding as pulumi-nodejs:dynamic:Resource. The new provider's URNs
are chatwoot:index:AutomationRule etc. Pulumi can't auto-migrate URNs
across providers, so a one-shot stack-state edit is required per stack.
The runbook lives in the PR description; verify with pnpm preview
afterwards (must show zero diff).
References
- Provider source:
provider/ - Chatwoot automation rules controller (source of truth for the
request/response schema):
app/controllers/api/v1/accounts/automation_rules_controller.rb - Chatwoot automation rule model (allowed
attribute_key/action_namevalues):app/models/automation_rule.rb - Chatwoot agent bots controller:
app/controllers/api/v1/accounts/agent_bots_controller.rb - Agent Bot API reference: https://developers.chatwoot.com/api-reference/agentbots/create-an-agent-bot