Skip to content
Back to skills

Intercom Reference Architecture

ASecurity

'Implement Intercom reference architecture with layered project structure.

  • 2,688 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 29, 2026
ai-agentstypescriptexpressapidatabase

Works with

  • claude code
  • cli
  • api

Security analysis

A100/100

Scanned May 29, 2026

npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill intercom-reference-architecture --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Intercom Reference Architecture?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Intercom Reference Architecture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jeremylongshore-intercom-reference-architecture/badge)](https://www.skillsdirectory.com/skills/jeremylongshore-intercom-reference-architecture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: intercom-reference-architecture
description: 'Implement Intercom reference architecture with layered project structure.

  Use when designing new Intercom integrations, reviewing project structure,

  or establishing architecture standards for Intercom applications.

  Trigger with phrases like "intercom architecture", "intercom project structure",

  "how to organize intercom", "intercom layout", "intercom design patterns".

  '
allowed-tools: Read, Grep
version: 1.0.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
tags:
- saas
- support
- messaging
- intercom
compatibility: Designed for Claude Code
---
# 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

```typescript
// 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

```typescript
// 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

```typescript
// 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)

```typescript
// 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 | `resetClient()` in beforeEach |

## Resources

- [Intercom API Reference](https://developers.intercom.com/docs/references/rest-api/api.intercom.io)
- [Articles API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/articles)
- [Help Center API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/help-center)
- [intercom-client npm](https://www.npmjs.com/package/intercom-client)

## Next Steps

For multi-environment setup, see `intercom-multi-env-setup`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…