Claude-code-plugins-plus-skills intercom-reference-architecture
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/intercom-pack/skills/intercom-reference-architecture" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-intercom-reference-architecture && rm -rf "$T"
manifest:
plugins/saas-packs/intercom-pack/skills/intercom-reference-architecture/SKILL.mdsource content
Intercom Reference Architecture
Overview
Production-ready architecture for Intercom integrations with layered separation, type-safe SDK usage, webhook processing, contact sync, and Help Center management.
Project Structure
my-intercom-app/ ├── src/ │ ├── intercom/ │ │ ├── client.ts # Singleton IntercomClient wrapper │ │ ├── types.ts # Extended Intercom types │ │ └── errors.ts # Custom error classes │ ├── services/ │ │ ├── contacts.service.ts # Contact CRUD + search + merge │ │ ├── conversations.service.ts # Conversation lifecycle │ │ ├── articles.service.ts # Help Center article management │ │ └── events.service.ts # Data event tracking │ ├── webhooks/ │ │ ├── router.ts # Topic-based event routing │ │ ├── signature.ts # X-Hub-Signature verification │ │ └── handlers/ │ │ ├── conversation.handler.ts │ │ └── contact.handler.ts │ ├── sync/ │ │ ├── contact-sync.ts # CRM <-> Intercom contact sync │ │ └── company-sync.ts # Company data sync │ ├── api/ │ │ ├── health.ts # Health check endpoint │ │ └── webhooks.ts # Webhook endpoint │ └── cache/ │ └── intercom-cache.ts # LRU + Redis caching layer ├── tests/ │ ├── unit/ │ │ ├── contacts.test.ts │ │ └── webhooks.test.ts │ └── integration/ │ └── intercom.integration.test.ts ├── config/ │ ├── development.json │ ├── staging.json │ └── production.json └── package.json
Layer Architecture
┌─────────────────────────────────────────────┐ │ API / Webhook Layer │ │ Express routes, webhook endpoints │ ├─────────────────────────────────────────────┤ │ Service Layer │ │ contacts.service, conversations.service │ │ Business logic, orchestration │ ├─────────────────────────────────────────────┤ │ Intercom Client Layer │ │ intercom-client SDK, error handling │ │ Caching, rate limit management │ ├─────────────────────────────────────────────┤ │ Infrastructure │ │ Redis cache, job queue, monitoring │ └─────────────────────────────────────────────┘
Instructions
Step 1: Client Layer
// src/intercom/client.ts import { IntercomClient, IntercomError } 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 required"); instance = new IntercomClient({ token }); } return instance; } // Typed error wrapper export class IntercomServiceError extends Error { constructor( message: string, public readonly statusCode: number, public readonly code: string, public readonly retryable: boolean, public readonly requestId?: string ) { super(message); this.name = "IntercomServiceError"; } static from(err: unknown): IntercomServiceError { if (err instanceof IntercomError) { const retryable = err.statusCode === 429 || (err.statusCode ?? 0) >= 500; return new IntercomServiceError( err.message, err.statusCode ?? 500, err.body?.errors?.[0]?.code ?? "unknown", retryable, err.body?.request_id ); } return new IntercomServiceError( (err as Error).message, 500, "internal", false ); } }
Step 2: Contacts Service
// src/services/contacts.service.ts import { getClient, IntercomServiceError } from "../intercom/client"; import { Intercom } from "intercom-client"; export class ContactsService { private client = getClient(); async findOrCreate(params: { email: string; externalId: string; name?: string; customAttributes?: Record<string, any>; }): Promise<Intercom.Contact> { // Search first to avoid 409 conflicts const existing = await this.client.contacts.search({ query: { field: "external_id", operator: "=", value: params.externalId }, }); if (existing.data.length > 0) { return existing.data[0]; } return this.client.contacts.create({ role: "user", externalId: params.externalId, email: params.email, name: params.name, customAttributes: params.customAttributes, }); } async syncFromCRM(crmUser: { id: string; email: string; name: string; plan: string; company: string; }): Promise<Intercom.Contact> { const contact = await this.findOrCreate({ email: crmUser.email, externalId: crmUser.id, name: crmUser.name, customAttributes: { plan: crmUser.plan, company_name: crmUser.company, last_synced_at: Math.floor(Date.now() / 1000), }, }); return contact; } async mergeLead(leadId: string, userId: string): Promise<Intercom.Contact> { return this.client.contacts.merge({ from: leadId, into: userId }); } async *searchAll( query: Intercom.SearchRequest["query"] ): AsyncGenerator<Intercom.Contact> { let startingAfter: string | undefined; do { const page = await this.client.contacts.search({ query, pagination: { per_page: 50, starting_after: startingAfter }, }); for (const contact of page.data) yield contact; startingAfter = page.pages?.next?.startingAfter ?? undefined; } while (startingAfter); } }
Step 3: Conversations Service
// src/services/conversations.service.ts import { getClient } from "../intercom/client"; export class ConversationsService { private client = getClient(); async replyAsAdmin( conversationId: string, adminId: string, body: string ): Promise<void> { await this.client.conversations.reply({ conversationId, type: "admin", adminId, body, }); } async addNote( conversationId: string, adminId: string, note: string ): Promise<void> { await this.client.conversations.reply({ conversationId, type: "note", adminId, body: note, }); } async closeWithMessage( conversationId: string, adminId: string, message?: string ): Promise<void> { await this.client.conversations.close({ conversationId, adminId, body: message, }); } async getOpenConversationsForAdmin(adminId: string) { return this.client.conversations.search({ query: { operator: "AND", value: [ { field: "state", operator: "=", value: "open" }, { field: "admin_assignee_id", operator: "=", value: adminId }, ], }, sort: { field: "updated_at", order: "descending" }, }); } }
Step 4: Articles Service (Help Center)
// src/services/articles.service.ts import { getClient } from "../intercom/client"; export class ArticlesService { private client = getClient(); async createArticle(params: { title: string; body: string; authorId: string; parentId?: string; // Collection ID state?: "published" | "draft"; }) { return this.client.articles.create({ title: params.title, body: params.body, authorId: params.authorId, parentId: params.parentId, state: params.state || "draft", }); } async *listAll() { const response = await this.client.articles.list(); for await (const article of response) { yield article; } } async listCollections() { return this.client.helpCenter.listCollections(); } }
Step 5: Data Flow
┌──────────────┐ Webhook POST ┌───────────────┐ │ Intercom │ ─────────────────▶ │ Webhook │ │ Platform │ │ Router │ │ │ ◀── API calls ──── │ │ └──────────────┘ └───────┬───────┘ │ ┌────────▼────────┐ │ Service Layer │ │ - Contacts │ │ - Conversations │ │ - Articles │ └────────┬────────┘ │ ┌────────▼────────┐ │ Your Database │ │ + Cache (Redis)│ └─────────────────┘
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Circular dependencies | Service A imports B imports A | Use dependency injection |
| Client initialization race | Async token fetch | Lazy singleton pattern |
| Cache inconsistency | Stale data after update | Webhook-driven invalidation |
| Test isolation | Shared SDK state | in beforeEach |
Resources
Next Steps
For multi-environment setup, see
intercom-multi-env-setup.