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 token | Chatwoot API |
|---|---|
chatwoot:index:AutomationRule | /api/v1/accounts/{acc}/automation_rules |
chatwoot:index:AgentBot | /api/v1/accounts/{acc}/agent_bots |
chatwoot:index:InboxAgentBot | POST /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:TeamMember | POST/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).WorkingHourssub-resource — folded intoInboxconfig later if needed.CustomView/CustomFilter— niche per-agent UI config.Portal+Category+Article— help-center hierarchy; deserves its own PR.- Inbox
emailchannel creation — Chatwoot's email-creation flow runs IMAP validation through a different endpoint. Existing email inboxes can be adopted viapulumi importand 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:
- Pulumi config:
chatwoot:apiUrl/chatwoot:apiKey(the latter issecret). - 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
pulumi-go-provider: https://github.com/pulumi/pulumi-go-provider- Chatwoot API overview: https://www.chatwoot.com/developers/api/
- Authoritative when the docs lag: the controllers in
chatwoot/chatwootatapp/controllers/api/v1/accounts/. Field-level shapes for nested types (channel config, condition operators) are best cross-referenced against the model files atapp/models/.