Intercom Local Dev Loop
Overview
Set up a fast local development workflow for Intercom integrations with proper
test isolation, mocking strategies, and webhook tunneling. The loop has two
lanes: a mocked unit lane that runs offline with no token, and an integration
lane that talks to a real dev workspace and is skipped automatically when no
token is present.
Prerequisites
- Completed
intercom-install-auth setup
- Node.js 18+ with npm/pnpm
- A test/development Intercom workspace (separate from production)
Authentication
The client authenticates with a single Intercom bearer access token, issued per
workspace by the intercom-install-auth step. Read it from
process.env.INTERCOM_ACCESS_TOKEN (loaded from git-ignored .env.development);
never hardcode it. The mocked unit lane needs no token at all — pointing the loop
at a different dev workspace is only a matter of swapping the .env.development
value.
Instructions
Work through these steps to stand up the loop. The full, copy-paste-ready code
for every step lives in the implementation walkthrough.
Scaffold the project structure — an src/intercom/ module (singleton
client.ts, plus contacts.ts / conversations.ts / types.ts), a tests/
tree with a mocks/ factory, and three env files (.env.example committed,
.env.development and .env.test git-ignored). Use Write to create each
file. See implementation.md.
Configure environments — commit .env.example as the template and keep
real tokens in the git-ignored .env.development. See
implementation.md.
Write an environment-aware client singleton that reads the token, throws a
clear error when it is missing, and exposes a resetClient() for tests. The
skeleton:
// src/intercom/client.ts
import { IntercomClient } from "intercom-client";
let instance: IntercomClient | null = null;
export function getClient(): IntercomClient {
if (!instance) {
const token = process.env.INTERCOM_ACCESS_TOKEN;
if (!token) {
throw new Error(
"INTERCOM_ACCESS_TOKEN not set. Copy .env.example to .env.development"
);
}
instance = new IntercomClient({ token });
}
return instance;
}
export function resetClient(): void {
instance = null;
}
Build a mock client factory (tests/mocks/intercom.ts) covering contacts,
conversations, messages, admins, and tags with vi.fn() resolved values, so
the unit lane never touches the network. Full factory in
implementation.md.
Write mocked unit tests against the factory, asserting call arguments and
returned shapes. See implementation.md.
Tunnel webhooks with ngrok — run the local server, ngrok http 3000, and
register the HTTPS URL in Intercom Developer Hub. Use Bash(npx:*) for
ngrok. See implementation.md.
Wire package scripts (dev, test, test:watch, test:integration,
typecheck) — use Edit to add them to package.json, then drive the loop
with Bash(npm:*). See implementation.md.
Output
Following this skill produces a working local Intercom development loop:
- A scaffolded
src/intercom/ client module and tests/ tree with a reusable
mock factory.
- Three environment files — a committed
.env.example template plus git-ignored
.env.development / .env.test.
- An offline mocked unit test lane (
npm run test / test:watch) that runs with
no token and no network.
- A token-gated integration lane (
npm run test:integration) that is skipped
automatically when INTERCOM_ACCESS_TOKEN is absent.
- An ngrok webhook tunnel exposing the local server to Intercom's Developer Hub.
Error Handling
| Error |
Cause |
Solution |
INTERCOM_ACCESS_TOKEN not set |
Missing .env file |
Copy .env.example to .env.development |
| Port 3000 in use |
Another process |
lsof -i :3000 and kill, or change port |
| ngrok tunnel expired |
Free tier 2h limit |
Restart ngrok or use paid plan |
| Mock type mismatch |
SDK updated |
Regenerate mocks from SDK types |
rate_limit_exceeded in dev |
Dev workspace limits |
Add delays between integration tests |
Examples
A minimal mocked unit test (from
implementation.md):
it("should create a user contact", async () => {
const contact = await mockClient.contacts.create({
role: "user",
externalId: "user-123",
email: "test@example.com",
});
expect(contact.id).toBe("mock-contact-id");
expect(mockClient.contacts.create).toHaveBeenCalledOnce();
});
For the token-gated integration test pattern (describe.skipIf, live create +
cleanup) and the commands that drive each lane, see
the examples reference.
Resources
- Full implementation walkthrough — every step's complete code
- Integration examples — live-workspace test pattern and run commands
- intercom-client npm
- Vitest Documentation
- ngrok
- See
intercom-sdk-patterns for production-ready code patterns.
Source: jeremylongshore/claude-code-plugins-plus-skills → plugins/saas-packs/intercom-pack/skills/intercom-local-dev-loop/SKILL.md
1---2name: intercom-local-dev-loop3description: "| Configure Intercom local development with testing, mocking, and hot reload. Use when setting up a development environment, writing tests against the Intercom API, or establishing a fast iteration cycle against a dev workspace. Trigger with phrases like \"intercom dev setup\", \"intercom local development\", \"intercom dev environment\", \"develop with intercom\", \"test intercom locally\"."4allowed-tools: "Read, Write, Edit, Bash(npm:*), Bash(npx:*)"5category: testing-and-qa6source_repo: jeremylongshore/claude-code-plugins-plus-skills7source_path: "plugins/saas-packs/intercom-pack/skills/intercom-local-dev-loop/SKILL.md"8source_url: https://github.com/jeremylongshore/claude-code-plugins-plus-skills/blob/HEAD/plugins/saas-packs/intercom-pack/skills/intercom-local-dev-loop/SKILL.md9---1011# Intercom Local Dev Loop1213## Overview1415Set up a fast local development workflow for Intercom integrations with proper16test isolation, mocking strategies, and webhook tunneling. The loop has two17lanes: a mocked unit lane that runs offline with no token, and an integration18lane that talks to a real dev workspace and is skipped automatically when no19token is present.2021## Prerequisites2223- Completed `intercom-install-auth` setup24- Node.js 18+ with npm/pnpm25- A test/development Intercom workspace (separate from production)2627## Authentication2829The client authenticates with a single Intercom bearer access token, issued per30workspace by the `intercom-install-auth` step. Read it from31`process.env.INTERCOM_ACCESS_TOKEN` (loaded from git-ignored `.env.development`);32never hardcode it. The mocked unit lane needs no token at all — pointing the loop33at a different dev workspace is only a matter of swapping the `.env.development`34value.3536## Instructions3738Work through these steps to stand up the loop. The full, copy-paste-ready code39for every step lives in [the implementation walkthrough](references/implementation.md).40411. **Scaffold the project structure** — an `src/intercom/` module (singleton42 `client.ts`, plus `contacts.ts` / `conversations.ts` / `types.ts`), a `tests/`43 tree with a `mocks/` factory, and three env files (`.env.example` committed,44 `.env.development` and `.env.test` git-ignored). Use **Write** to create each45 file. See [implementation.md](references/implementation.md).46472. **Configure environments** — commit `.env.example` as the template and keep48 real tokens in the git-ignored `.env.development`. See49 [implementation.md](references/implementation.md).50513. **Write an environment-aware client singleton** that reads the token, throws a52 clear error when it is missing, and exposes a `resetClient()` for tests. The53 skeleton:5455 ```typescript56 // src/intercom/client.ts57 import { IntercomClient } from "intercom-client";5859 let instance: IntercomClient | null = null;6061 export function getClient(): IntercomClient {62 if (!instance) {63 const token = process.env.INTERCOM_ACCESS_TOKEN;64 if (!token) {65 throw new Error(66 "INTERCOM_ACCESS_TOKEN not set. Copy .env.example to .env.development"67 );68 }69 instance = new IntercomClient({ token });70 }71 return instance;72 }7374 export function resetClient(): void {75 instance = null;76 }77 ```78794. **Build a mock client factory** (`tests/mocks/intercom.ts`) covering contacts,80 conversations, messages, admins, and tags with `vi.fn()` resolved values, so81 the unit lane never touches the network. Full factory in82 [implementation.md](references/implementation.md).83845. **Write mocked unit tests** against the factory, asserting call arguments and85 returned shapes. See [implementation.md](references/implementation.md).86876. **Tunnel webhooks with ngrok** — run the local server, `ngrok http 3000`, and88 register the HTTPS URL in Intercom Developer Hub. Use **Bash(npx:*)** for89 ngrok. See [implementation.md](references/implementation.md).90917. **Wire package scripts** (`dev`, `test`, `test:watch`, `test:integration`,92 `typecheck`) — use **Edit** to add them to `package.json`, then drive the loop93 with **Bash(npm:*)**. See [implementation.md](references/implementation.md).9495## Output9697Following this skill produces a working local Intercom development loop:9899- A scaffolded `src/intercom/` client module and `tests/` tree with a reusable100 mock factory.101- Three environment files — a committed `.env.example` template plus git-ignored102 `.env.development` / `.env.test`.103- An offline mocked unit test lane (`npm run test` / `test:watch`) that runs with104 no token and no network.105- A token-gated integration lane (`npm run test:integration`) that is skipped106 automatically when `INTERCOM_ACCESS_TOKEN` is absent.107- An ngrok webhook tunnel exposing the local server to Intercom's Developer Hub.108109## Error Handling110111| Error | Cause | Solution |112|-------|-------|----------|113| `INTERCOM_ACCESS_TOKEN not set` | Missing .env file | Copy `.env.example` to `.env.development` |114| Port 3000 in use | Another process | `lsof -i :3000` and kill, or change port |115| ngrok tunnel expired | Free tier 2h limit | Restart ngrok or use paid plan |116| Mock type mismatch | SDK updated | Regenerate mocks from SDK types |117| `rate_limit_exceeded` in dev | Dev workspace limits | Add delays between integration tests |118119## Examples120121A minimal mocked unit test (from122[implementation.md](references/implementation.md)):123124```typescript125it("should create a user contact", async () => {126 const contact = await mockClient.contacts.create({127 role: "user",128 externalId: "user-123",129 email: "test@example.com",130 });131132 expect(contact.id).toBe("mock-contact-id");133 expect(mockClient.contacts.create).toHaveBeenCalledOnce();134});135```136137For the token-gated integration test pattern (`describe.skipIf`, live create +138cleanup) and the commands that drive each lane, see139[the examples reference](references/examples.md).140141## Resources142143- [Full implementation walkthrough](references/implementation.md) — every step's complete code144- [Integration examples](references/examples.md) — live-workspace test pattern and run commands145- [intercom-client npm](https://www.npmjs.com/package/intercom-client)146- [Vitest Documentation](https://vitest.dev/)147- [ngrok](https://ngrok.com/)148- See `intercom-sdk-patterns` for production-ready code patterns.149150---151152**Source:** [`jeremylongshore/claude-code-plugins-plus-skills`](https://github.com/jeremylongshore/claude-code-plugins-plus-skills) → `plugins/saas-packs/intercom-pack/skills/intercom-local-dev-loop/SKILL.md`