internal/chatwoot/provider/README.md

studyflash-chatwoot-provider

Pulumi-native resource provider for the self-hosted Chatwoot at support.studyflash.ch (account 2). Implemented in Go via pulumi-go-provider. Ships a real plugin binary (pulumi-resource-chatwoot) and a generated TypeScript SDK at sdk/nodejs/, consumed by the parent stack at internal/chatwoot/ via workspace:*.

studyflash-chatwoot-provider

Pulumi-native resource provider for the self-hosted Chatwoot at support.studyflash.ch (account 2). Implemented in Go via pulumi-go-provider. Ships a real plugin binary (pulumi-resource-chatwoot) and a generated TypeScript SDK at sdk/nodejs/, consumed by the parent stack at internal/chatwoot/ via workspace:*.

This replaces a Pulumi Dynamic Provider that lived directly inside internal/chatwoot/ (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 native implementation has none of those gaps: read works, pulumi import and pulumi refresh work, and provider edits are a no-op against existing state.

Resources (v0)

Pulumi tokenChatwoot API
chatwoot:index:AutomationRule/api/v1/accounts/{acc}/automation_rules
chatwoot:index:AgentBot/api/v1/accounts/{acc}/agent_bots
chatwoot:index:InboxAgentBotPOST /api/v1/accounts/{acc}/inbox/{id}/set_agent_bot + GET .../inboxes/{id} (drift)
chatwoot:index:Webhook/api/v1/accounts/{acc}/webhooks
chatwoot:index:Inbox/api/v1/accounts/{acc}/inboxes
chatwoot:index:Team/api/v1/accounts/{acc}/teams
chatwoot:index:TeamMemberPOST/DELETE /api/v1/accounts/{acc}/teams/{id}/team_members (binding)
chatwoot:index:Label/api/v1/accounts/{acc}/labels
chatwoot:index:CustomAttribute/api/v1/accounts/{acc}/custom_attribute_definitions
chatwoot:index:CannedResponse/api/v1/accounts/{acc}/canned_responses
chatwoot:index:SlaPolicy/api/v1/accounts/{acc}/sla_policies

InboxAgentBot and TeamMember are bindings — they own a single relation each (an inbox-to-agent_bot pointer, a team-to-user pointer). Modeling those as separate resources keeps the parent (Inbox, Team) stable across binding churn rather than forcing a replace whenever the relation changes.

Out of scope for v0

  • Integration (account-level Slack/Linear/Dialogflow).
  • WorkingHours sub-resource — folded into Inbox config later if needed.
  • CustomView / CustomFilter — niche per-agent UI config.
  • Portal + Category + Article — help-center hierarchy; deserves its own PR.
  • Inbox email channel creation — Chatwoot's email-creation flow runs IMAP validation through a different endpoint. Existing email inboxes can be adopted via pulumi import and updated, just not created.
  • All runtime data — conversations, messages, contacts, dashboards, reports.

payload envelope handling

Several Chatwoot endpoints (notably the automation_rules controller, plus labels and sla_policies) wrap the response in {"payload": <body>}, while others (agent_bots, single inbox/team) return the bare object. The dynamic-provider TypeScript code didn't account for this and read r.id directly, which is one reason pulumi refresh was unreliable. The Go client transparently unwraps payload if present, so resources consume a flat shape regardless.

Build / regenerate

The SDK is checked in (TS sources only — bin/, node_modules/, and the provider binary are gitignored). Regenerate after any change to a Go file:

make sdk            # build binary, dump schema, regen + build TS SDK
make install-plugin # symlink the binary into ~/.pulumi/plugins

The Makefile patches the gen-sdk output to add main / types / files fields and to copy package.json into bin/ so module resolution works from a Pulumi runtime that requires the compiled artefacts.

Consuming from a sibling stack

The SDK exposes itself as studyflash-chatwoot-provider via the generated sdk/nodejs/package.json. pnpm picks it up through the internal/chatwoot workspace entry — note the consumer (internal/chatwoot/) and the SDK (internal/chatwoot/provider/sdk/nodejs/) live in nested directories, so the consumer's package.json lists the provider via workspace:* and pnpm resolves it through the nested package.json:

{
  "dependencies": {
    "studyflash-chatwoot-provider": "workspace:*"
  }
}
import * as chatwoot from 'studyflash-chatwoot-provider';

const accountId = '2';

const maximilian = new chatwoot.AgentBot('maximilian', {
  accountId,
  name: 'Maximilian',
  outgoingUrl: 'https://support-bot.studyflash.dev/chatwoot-webhook',
});

new chatwoot.InboxAgentBot('maximilian-support-mailbox', {
  accountId,
  inboxId: 1,
  agentBotId: maximilian.agentBotId,
});

new chatwoot.AutomationRule('rxdb-bug-on-create', {
  accountId,
  name: 'RxDb Bug',
  description: 'Short fix',
  eventName: 'conversation_created',
  active: true,
  conditions: [
    {
      attributeKey: 'email',
      filterOperator: 'contains',
      values: ['RxDB Error-Code: DB6'],
    },
  ],
  actions: [
    { actionName: 'send_message', actionParams: ['Hi there, …'] },
  ],
});

Adopting existing live resources

Read is fully implemented, so adoption uses the standard Pulumi mechanism — pass import: '<chatwoot-id>' in resource options on the first pulumi up:

new chatwoot.AgentBot(
  'maximilian',
  { /* declared shape */ },
  { import: '1' },
);

For InboxAgentBot and TeamMember, use the composite ID:

new chatwoot.InboxAgentBot('maximilian-support-mailbox', {
  accountId, inboxId: 1, agentBotId: maximilian.agentBotId,
}, { import: '2/1' });          // <accountId>/<inboxId>

new chatwoot.TeamMember('rajiv-on-engineering', {
  accountId, teamId: 1, userId: 2,
}, { import: '2/1/2' });        // <accountId>/<teamId>/<userId>

Pulumi calls Read(ctx, id), populates state from live Chatwoot, and the resource is managed normally afterwards. Once pulumi preview shows zero diff, drop the import: line from the declaration.

Credentials

The provider reads two values, in priority order:

  1. Pulumi config: chatwoot:apiUrl / chatwoot:apiKey (the latter is secret).
  2. Env vars: CHATWOOT_API_URL / CHATWOOT_API_KEY (used as defaults).

internal/chatwoot/'s pnpm preview / pnpm up scripts already wrap infisical run --path=/internal/chatwoot/ -- pulumi …, so the env vars reach the provider without ever touching disk.

Layout

internal/chatwoot/provider/
├── main.go              # provider boot
├── client.go            # Chatwoot HTTP client (with payload unwrap)
├── resources.go         # AutomationRule / AgentBot / InboxAgentBot / Webhook
├── resources_extra.go   # Inbox / Team / TeamMember / Label / CustomAttribute / CannedResponse / SlaPolicy
├── go.mod / go.sum
├── Makefile             # build / schema / gen-sdk / install-plugin
├── README.md
└── sdk/nodejs/          # generated TS SDK (committed; bin/ + node_modules/ gitignored)

References