internal/chatwoot/README.md

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 labelled dev-support to Rajiv (currently inactive).
  • rxdb-bug-on-create — auto-replies on conversation_created when the contact email contains RxDB Error-Code: DB6.
  • rxdb-bug-on-open — auto-replies on conversation_opened when the contact email contains RxDB 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 legacy n8n-legacy webhook as the live event path. Maximilian remains as reproducible config and its accessToken is 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 (id 1) — 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 (id 5) — API channel; webhook_url points at the production studyflash-core-api worker. (Was previously pointing at a stale PR-1959 preview URL — fixed by this PR.)

Webhooks

  • n8n-legacy (id 1, Chatwoot name N8N) — historical account-level webhook delivering message_created events 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

NameTypeNotes
maximilianAccessTokensecret stringThe 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

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.)

VarWhere it comes fromWhy it's needed
CHATWOOT_API_URLInfisical (https://support.studyflash.ch)Base URL for the REST calls
CHATWOOT_API_KEYInfisical (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