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.md
source 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

IssueCauseSolution
Circular dependenciesService A imports B imports AUse dependency injection
Client initialization raceAsync token fetchLazy singleton pattern
Cache inconsistencyStale data after updateWebhook-driven invalidation
Test isolationShared SDK state
resetClient()
in beforeEach

Resources

Next Steps

For multi-environment setup, see

intercom-multi-env-setup
.