[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6F7HS53R9TW284HT1BQPHDH":3,"skill-same:01M6F7HS53R9TW284HT1BQPHDH":74},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":51,"commitSha":52,"skillDirectory":53,"forksCount":54,"repositoryHtmlUrl":55,"repositoryHomepageUrl":56,"ownerHtmlUrl":57,"ownerBlogUrl":6,"ownerDescription":58,"updatedAt":59,"install":60,"resources":73},"01M6F7HS53R9TW284HT1BQPHDH","attio-reference-architecture",null,"Production reference architecture for Attio CRM integrations -- layered\nproject structure, sync patterns, webhook processing, and multi-environment setup.\nTrigger: \"attio architecture\", \"attio best practices\", \"attio project structure\",\n\"how to organize attio\", \"attio integration design\".","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",67,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fjeremylongshore\u002Fclaude-code-plugins-plus-skills\u002Fattio-reference-architecture\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"attio reference architecture","architecture","reference","attio","integration design","processing","how to","io","webhook","project","structure","organize","crm","design","patterns","read","how","code","sync","project structure","2026-11-10 10:12:30",false,"---\nname: attio-reference-architecture\ndescription: |\n  Production reference architecture for Attio CRM integrations -- layered\n  project structure, sync patterns, webhook processing, and multi-environment setup.\n  Trigger: \"attio architecture\", \"attio best practices\", \"attio project structure\",\n  \"how to organize attio\", \"attio integration design\".\nallowed-tools: Read, Grep\nversion: 1.0.0\nlicense: MIT\nauthor: Jeremy Longshore \u003Cjeremy@intentsolutions.io>\ntags: [saas, crm, attio]\ncompatible-with: claude-code\n---\n\n# Attio Reference Architecture\n\n## Overview\n\nProduction architecture for applications that integrate with the Attio REST API (`https:\u002F\u002Fapi.attio.com\u002Fv2`). Covers project layout, layered service design, sync patterns, and operational concerns.\n\n## Project Structure\n\n```\nmy-attio-integration\u002F\n├── src\u002F\n│   ├── attio\u002F                        # Attio API layer (isolated)\n│   │   ├── client.ts                 # Typed fetch wrapper with retry\n│   │   ├── types.ts                  # Attio API types (AttioRecord, AttioError, etc.)\n│   │   ├── config.ts                 # Environment-based config loader\n│   │   └── errors.ts                 # AttioApiError class\n│   ├── services\u002F                     # Business logic (uses attio\u002F layer)\n│   │   ├── contacts.ts              # People\u002Fcompany sync logic\n│   │   ├── pipeline.ts              # Deal pipeline management\n│   │   ├── activity.ts              # Notes, tasks, comments\n│   │   └── sync.ts                  # Bi-directional sync orchestrator\n│   ├── webhooks\u002F                     # Incoming webhook handlers\n│   │   ├── router.ts                # Event type routing\n│   │   ├── verify.ts                # Signature verification\n│   │   └── handlers\u002F\n│   │       ├── record-events.ts     # record.created\u002Fupdated\u002Fdeleted\u002Fmerged\n│   │       ├── entry-events.ts      # list-entry.created\u002Fupdated\u002Fdeleted\n│   │       └── activity-events.ts   # note\u002Ftask\u002Fcomment events\n│   ├── api\u002F                         # Outbound API routes\n│   │   ├── health.ts                # Health check (includes Attio)\n│   │   └── webhooks.ts              # Webhook receiver endpoint\n│   ├── cache\u002F                       # Caching layer\n│   │   ├── schema-cache.ts          # Object\u002Fattribute definitions (30min TTL)\n│   │   └── record-cache.ts          # Record data (5min TTL, webhook invalidation)\n│   └── index.ts                     # App entrypoint\n├── tests\u002F\n│   ├── mocks\u002F                       # MSW handlers for Attio API\n│   ├── unit\u002F                        # Service logic tests (mocked API)\n│   └── integration\u002F                 # Live API tests (CI-gated)\n├── config\u002F\n│   ├── attio.development.json\n│   ├── attio.staging.json\n│   └── attio.production.json\n├── .env.example\n└── .github\u002Fworkflows\u002Fattio.yml\n```\n\n## Layered Architecture\n\n```\n┌──────────────────────────────────────────────────┐\n│  API Layer (routes, webhook endpoint)            │\n│  - Receives HTTP requests                        │\n│  - Validates webhook signatures                  │\n│  - Returns health status                         │\n├──────────────────────────────────────────────────┤\n│  Service Layer (business logic)                  │\n│  - Contact sync, pipeline management             │\n│  - Bi-directional data mapping                   │\n│  - Event-driven automations                      │\n├──────────────────────────────────────────────────┤\n│  Attio Layer (API client, types, errors)         │\n│  - Typed fetch wrapper with retry                │\n│  - Error normalization (AttioApiError)            │\n│  - Pagination helpers                            │\n├──────────────────────────────────────────────────┤\n│  Infrastructure Layer (cache, queue, monitoring)  │\n│  - LRU + Redis caching with webhook invalidation │\n│  - Rate limit queue (p-queue)                    │\n│  - Structured logging and metrics                │\n└──────────────────────────────────────────────────┘\n```\n\n**Rule:** Each layer only calls the layer directly below it. The API layer never calls the Attio client directly.\n\n## Core Components\n\n### Component 1: Service Layer Facade\n\n```typescript\n\u002F\u002F src\u002Fservices\u002Fcontacts.ts\nimport { AttioClient } from \"..\u002Fattio\u002Fclient\";\nimport { cachedGet, invalidateRecord } from \"..\u002Fcache\u002Frecord-cache\";\nimport type { AttioRecord } from \"..\u002Fattio\u002Ftypes\";\n\nexport class ContactService {\n  constructor(private client: AttioClient) {}\n\n  async findByEmail(email: string): Promise\u003CAttioRecord | null> {\n    const res = await this.client.post\u003C{ data: AttioRecord[] }>(\n      \"\u002Fobjects\u002Fpeople\u002Frecords\u002Fquery\",\n      {\n        filter: { email_addresses: email },\n        limit: 1,\n      }\n    );\n    return res.data[0] || null;\n  }\n\n  async upsertPerson(data: {\n    email: string;\n    firstName: string;\n    lastName: string;\n    company?: string;\n  }): Promise\u003CAttioRecord> {\n    \u002F\u002F Use PUT (assert) for idempotent upsert\n    const res = await this.client.put\u003C{ data: AttioRecord }>(\n      \"\u002Fobjects\u002Fpeople\u002Frecords\",\n      {\n        data: {\n          values: {\n            email_addresses: [data.email],\n            name: [{\n              first_name: data.firstName,\n              last_name: data.lastName,\n              full_name: `${data.firstName} ${data.lastName}`,\n            }],\n            ...(data.company ? { company: [{ target_object: \"companies\", target_record_id: data.company }] } : {}),\n          },\n        },\n      }\n    );\n    return res.data;\n  }\n\n  async addToPipeline(\n    recordId: string,\n    listSlug: string,\n    stage: string,\n    value?: { currency: string; amount: number }\n  ): Promise\u003Cvoid> {\n    await this.client.post(`\u002Flists\u002F${listSlug}\u002Fentries`, {\n      data: {\n        parent_record_id: recordId,\n        parent_object: \"people\",\n        values: {\n          stage: [{ status: stage }],\n          ...(value ? {\n            deal_value: [{ currency_code: value.currency, currency_value: value.amount }],\n          } : {}),\n        },\n      },\n    });\n  }\n\n  async addNote(recordId: string, title: string, content: string): Promise\u003Cvoid> {\n    await this.client.post(\"\u002Fnotes\", {\n      data: {\n        parent_object: \"people\",\n        parent_record_id: recordId,\n        title,\n        format: \"markdown\",\n        content,\n      },\n    });\n  }\n}\n```\n\n### Component 2: Webhook Event Router\n\n```typescript\n\u002F\u002F src\u002Fwebhooks\u002Frouter.ts\nimport type { AttioWebhookEvent } from \"..\u002Fattio\u002Ftypes\";\n\ntype EventHandler = (event: AttioWebhookEvent) => Promise\u003Cvoid>;\n\nexport class WebhookRouter {\n  private handlers = new Map\u003Cstring, EventHandler[]>();\n\n  on(eventType: string, handler: EventHandler): void {\n    const existing = this.handlers.get(eventType) || [];\n    this.handlers.set(eventType, [...existing, handler]);\n  }\n\n  async route(event: AttioWebhookEvent): Promise\u003Cvoid> {\n    const handlers = this.handlers.get(event.event_type) || [];\n    if (handlers.length === 0) {\n      console.log(`No handler for event: ${event.event_type}`);\n      return;\n    }\n    await Promise.allSettled(handlers.map((h) => h(event)));\n  }\n}\n\n\u002F\u002F Usage\nconst router = new WebhookRouter();\nrouter.on(\"record.created\", async (event) => {\n  if (event.object?.api_slug === \"people\") {\n    await syncNewContactToExternalCRM(event.record!.id.record_id);\n  }\n});\nrouter.on(\"record.updated\", async (event) => {\n  invalidateRecord(event.record!.id.record_id);\n});\nrouter.on(\"list-entry.created\", async (event) => {\n  await triggerPipelineAutomation(event);\n});\n```\n\n### Component 3: Bi-Directional Sync\n\n```typescript\n\u002F\u002F src\u002Fservices\u002Fsync.ts\nexport class AttioSyncService {\n  private lastSyncCursor: string | null = null;\n\n  \u002F** Outbound: push local changes to Attio *\u002F\n  async pushToAttio(localContact: LocalContact): Promise\u003Cstring> {\n    const attioRecord = await this.contacts.upsertPerson({\n      email: localContact.email,\n      firstName: localContact.firstName,\n      lastName: localContact.lastName,\n    });\n    return attioRecord.id.record_id;\n  }\n\n  \u002F** Inbound: pull Attio changes to local (webhook-driven) *\u002F\n  async handleAttioChange(event: AttioWebhookEvent): Promise\u003Cvoid> {\n    if (event.event_type === \"record.updated\") {\n      const record = await this.client.get\u003C{ data: AttioRecord }>(\n        `\u002Fobjects\u002F${event.object!.api_slug}\u002Frecords\u002F${event.record!.id.record_id}`\n      );\n      await this.updateLocalFromAttio(record.data);\n    }\n  }\n\n  \u002F** Full sync: reconcile all records (run periodically or on demand) *\u002F\n  async fullSync(objectSlug: string): Promise\u003C{ created: number; updated: number }> {\n    let created = 0, updated = 0;\n    const PAGE_SIZE = 500;\n    let offset = 0;\n\n    while (true) {\n      const page = await this.client.post\u003C{ data: AttioRecord[] }>(\n        `\u002Fobjects\u002F${objectSlug}\u002Frecords\u002Fquery`,\n        { limit: PAGE_SIZE, offset }\n      );\n\n      for (const record of page.data) {\n        const existed = await this.upsertLocal(record);\n        existed ? updated++ : created++;\n      }\n\n      if (page.data.length \u003C PAGE_SIZE) break;\n      offset += PAGE_SIZE;\n    }\n\n    return { created, updated };\n  }\n}\n```\n\n### Component 4: Multi-Environment Config\n\n```typescript\n\u002F\u002F src\u002Fattio\u002Fconfig.ts\ninterface AttioEnvironmentConfig {\n  apiKey: string;\n  webhookSecret: string;\n  baseUrl: string;\n  cache: { schemaTtlMs: number; recordTtlMs: number };\n  rateLimit: { concurrency: number; intervalCap: number };\n}\n\nconst configs: Record\u003Cstring, Partial\u003CAttioEnvironmentConfig>> = {\n  development: {\n    cache: { schemaTtlMs: 60_000, recordTtlMs: 10_000 },\n    rateLimit: { concurrency: 2, intervalCap: 5 },\n  },\n  staging: {\n    cache: { schemaTtlMs: 300_000, recordTtlMs: 60_000 },\n    rateLimit: { concurrency: 5, intervalCap: 8 },\n  },\n  production: {\n    cache: { schemaTtlMs: 1_800_000, recordTtlMs: 300_000 },\n    rateLimit: { concurrency: 10, intervalCap: 15 },\n  },\n};\n\nexport function loadConfig(): AttioEnvironmentConfig {\n  const env = process.env.NODE_ENV || \"development\";\n  const envConfig = configs[env] || configs.development;\n  return {\n    apiKey: requireEnv(\"ATTIO_API_KEY\"),\n    webhookSecret: process.env.ATTIO_WEBHOOK_SECRET || \"\",\n    baseUrl: \"https:\u002F\u002Fapi.attio.com\u002Fv2\",\n    cache: envConfig.cache!,\n    rateLimit: envConfig.rateLimit!,\n  };\n}\n\nfunction requireEnv(key: string): string {\n  const val = process.env[key];\n  if (!val) throw new Error(`Missing required env: ${key}`);\n  return val;\n}\n```\n\n## Data Flow Diagram\n\n```\nExternal System                    Your Application                      Attio CRM\n     │                                   │                                  │\n     │  Local change ──────────────────▶ │                                  │\n     │                                   │  PUT \u002Fobjects\u002Fpeople\u002Frecords ──▶ │\n     │                                   │  ◀── 200 { data: record }       │\n     │                                   │                                  │\n     │                                   │       Webhook: record.updated    │\n     │                                   │  ◀──────────────────────────── │\n     │  ◀── Sync update ──────────────  │                                  │\n     │                                   │  GET \u002Fobjects\u002F...\u002Frecords\u002F... ─▶ │\n     │                                   │  ◀── 200 { data: record }       │\n```\n\n## Error Handling\n\n| Architecture issue | Symptom | Fix |\n|-------------------|---------|-----|\n| Service calls client directly | Tight coupling, hard to test | Add service layer facade |\n| No cache invalidation | Stale data after updates | Webhook-driven cache invalidation |\n| Sync conflicts | Both sides updated same record | Last-write-wins or conflict resolution queue |\n| No circuit breaker | Attio outage cascades | Add circuit breaker in Attio layer |\n\n## Resources\n\n- [Attio REST API Overview](https:\u002F\u002Fdocs.attio.com\u002Frest-api\u002Foverview)\n- [Attio Objects and Lists](https:\u002F\u002Fdocs.attio.com\u002Fdocs\u002Fobjects-and-lists)\n- [Attio Webhooks Guide](https:\u002F\u002Fdocs.attio.com\u002Frest-api\u002Fguides\u002Fwebhooks)\n- [Attio Developer Platform](https:\u002F\u002Fattio.com\u002Fplatform\u002Fdevelopers)\n\n## Next Steps\n\nThis is the capstone skill. For specific implementations, refer to the individual skills in this pack.\n\n","# Attio Reference Architecture\n\n## Overview\n\nProduction architecture for applications that integrate with the Attio REST API (`https:\u002F\u002Fapi.attio.com\u002Fv2`). Covers project layout, layered service design, sync patterns, and operational concerns.\n\n## Project Structure\n\n```\nmy-attio-integration\u002F\n├── src\u002F\n│   ├── attio\u002F                        # Attio API layer (isolated)\n│   │   ├── client.ts                 # Typed fetch wrapper with retry\n│   │   ├── types.ts                  # Attio API types (AttioRecord, AttioError, etc.)\n│   │   ├── config.ts                 # Environment-based config loader\n│   │   └── errors.ts                 # AttioApiError class\n│   ├── services\u002F                     # Business logic (uses attio\u002F layer)\n│   │   ├── contacts.ts              # People\u002Fcompany sync logic\n│   │   ├── pipeline.ts              # Deal pipeline management\n│   │   ├── activity.ts              # Notes, tasks, comments\n│   │   └── sync.ts                  # Bi-directional sync orchestrator\n│   ├── webhooks\u002F                     # Incoming webhook handlers\n│   │   ├── router.ts                # Event type routing\n│   │   ├── verify.ts                # Signature verification\n│   │   └── handlers\u002F\n│   │       ├── record-events.ts     # record.created\u002Fupdated\u002Fdeleted\u002Fmerged\n│   │       ├── entry-events.ts      # list-entry.created\u002Fupdated\u002Fdeleted\n│   │       └── activity-events.ts   # note\u002Ftask\u002Fcomment events\n│   ├── api\u002F                         # Outbound API routes\n│   │   ├── health.ts                # Health check (includes Attio)\n│   │   └── webhooks.ts              # Webhook receiver endpoint\n│   ├── cache\u002F                       # Caching layer\n│   │   ├── schema-cache.ts          # Object\u002Fattribute definitions (30min TTL)\n│   │   └── record-cache.ts          # Record data (5min TTL, webhook invalidation)\n│   └── index.ts                     # App entrypoint\n├── tests\u002F\n│   ├── mocks\u002F                       # MSW handlers for Attio API\n│   ├── unit\u002F                        # Service logic tests (mocked API)\n│   └── integration\u002F                 # Live API tests (CI-gated)\n├── config\u002F\n│   ├── attio.development.json\n│   ├── attio.staging.json\n│   └── attio.production.json\n├── .env.example\n└── .github\u002Fworkflows\u002Fattio.yml\n```\n\n## Layered Architecture\n\n```\n┌──────────────────────────────────────────────────┐\n│  API Layer (routes, webhook endpoint)            │\n│  - Receives HTTP requests                        │\n│  - Validates webhook signatures                  │\n│  - Returns health status                         │\n├──────────────────────────────────────────────────┤\n│  Service Layer (business logic)                  │\n│  - Contact sync, pipeline management             │\n│  - Bi-directional data mapping                   │\n│  - Event-driven automations                      │\n├──────────────────────────────────────────────────┤\n│  Attio Layer (API client, types, errors)         │\n│  - Typed fetch wrapper with retry                │\n│  - Error normalization (AttioApiError)            │\n│  - Pagination helpers                            │\n├──────────────────────────────────────────────────┤\n│  Infrastructure Layer (cache, queue, monitoring)  │\n│  - LRU + Redis caching with webhook invalidation │\n│  - Rate limit queue (p-queue)                    │\n│  - Structured logging and metrics                │\n└──────────────────────────────────────────────────┘\n```\n\n**Rule:** Each layer only calls the layer directly below it. The API layer never calls the Attio client directly.\n\n## Core Components\n\n### Component 1: Service Layer Facade\n\n```typescript\n\u002F\u002F src\u002Fservices\u002Fcontacts.ts\nimport { AttioClient } from \"..\u002Fattio\u002Fclient\";\nimport { cachedGet, invalidateRecord } from \"..\u002Fcache\u002Frecord-cache\";\nimport type { AttioRecord } from \"..\u002Fattio\u002Ftypes\";\n\nexport class ContactService {\n  constructor(private client: AttioClient) {}\n\n  async findByEmail(email: string): Promise\u003CAttioRecord | null> {\n    const res = await this.client.post\u003C{ data: AttioRecord[] }>(\n      \"\u002Fobjects\u002Fpeople\u002Frecords\u002Fquery\",\n      {\n        filter: { email_addresses: email },\n        limit: 1,\n      }\n    );\n    return res.data[0] || null;\n  }\n\n  async upsertPerson(data: {\n    email: string;\n    firstName: string;\n    lastName: string;\n    company?: string;\n  }): Promise\u003CAttioRecord> {\n    \u002F\u002F Use PUT (assert) for idempotent upsert\n    const res = await this.client.put\u003C{ data: AttioRecord }>(\n      \"\u002Fobjects\u002Fpeople\u002Frecords\",\n      {\n        data: {\n          values: {\n            email_addresses: [data.email],\n            name: [{\n              first_name: data.firstName,\n              last_name: data.lastName,\n              full_name: `${data.firstName} ${data.lastName}`,\n            }],\n            ...(data.company ? { company: [{ target_object: \"companies\", target_record_id: data.company }] } : {}),\n          },\n        },\n      }\n    );\n    return res.data;\n  }\n\n  async addToPipeline(\n    recordId: string,\n    listSlug: string,\n    stage: string,\n    value?: { currency: string; amount: number }\n  ): Promise\u003Cvoid> {\n    await this.client.post(`\u002Flists\u002F${listSlug}\u002Fentries`, {\n      data: {\n        parent_record_id: recordId,\n        parent_object: \"people\",\n        values: {\n          stage: [{ status: stage }],\n          ...(value ? {\n            deal_value: [{ currency_code: value.currency, currency_value: value.amount }],\n          } : {}),\n        },\n      },\n    });\n  }\n\n  async addNote(recordId: string, title: string, content: string): Promise\u003Cvoid> {\n    await this.client.post(\"\u002Fnotes\", {\n      data: {\n        parent_object: \"people\",\n        parent_record_id: recordId,\n        title,\n        format: \"markdown\",\n        content,\n      },\n    });\n  }\n}\n```\n\n### Component 2: Webhook Event Router\n\n```typescript\n\u002F\u002F src\u002Fwebhooks\u002Frouter.ts\nimport type { AttioWebhookEvent } from \"..\u002Fattio\u002Ftypes\";\n\ntype EventHandler = (event: AttioWebhookEvent) => Promise\u003Cvoid>;\n\nexport class WebhookRouter {\n  private handlers = new Map\u003Cstring, EventHandler[]>();\n\n  on(eventType: string, handler: EventHandler): void {\n    const existing = this.handlers.get(eventType) || [];\n    this.handlers.set(eventType, [...existing, handler]);\n  }\n\n  async route(event: AttioWebhookEvent): Promise\u003Cvoid> {\n    const handlers = this.handlers.get(event.event_type) || [];\n    if (handlers.length === 0) {\n      console.log(`No handler for event: ${event.event_type}`);\n      return;\n    }\n    await Promise.allSettled(handlers.map((h) => h(event)));\n  }\n}\n\n\u002F\u002F Usage\nconst router = new WebhookRouter();\nrouter.on(\"record.created\", async (event) => {\n  if (event.object?.api_slug === \"people\") {\n    await syncNewContactToExternalCRM(event.record!.id.record_id);\n  }\n});\nrouter.on(\"record.updated\", async (event) => {\n  invalidateRecord(event.record!.id.record_id);\n});\nrouter.on(\"list-entry.created\", async (event) => {\n  await triggerPipelineAutomation(event);\n});\n```\n\n### Component 3: Bi-Directional Sync\n\n```typescript\n\u002F\u002F src\u002Fservices\u002Fsync.ts\nexport class AttioSyncService {\n  private lastSyncCursor: string | null = null;\n\n  \u002F** Outbound: push local changes to Attio *\u002F\n  async pushToAttio(localContact: LocalContact): Promise\u003Cstring> {\n    const attioRecord = await this.contacts.upsertPerson({\n      email: localContact.email,\n      firstName: localContact.firstName,\n      lastName: localContact.lastName,\n    });\n    return attioRecord.id.record_id;\n  }\n\n  \u002F** Inbound: pull Attio changes to local (webhook-driven) *\u002F\n  async handleAttioChange(event: AttioWebhookEvent): Promise\u003Cvoid> {\n    if (event.event_type === \"record.updated\") {\n      const record = await this.client.get\u003C{ data: AttioRecord }>(\n        `\u002Fobjects\u002F${event.object!.api_slug}\u002Frecords\u002F${event.record!.id.record_id}`\n      );\n      await this.updateLocalFromAttio(record.data);\n    }\n  }\n\n  \u002F** Full sync: reconcile all records (run periodically or on demand) *\u002F\n  async fullSync(objectSlug: string): Promise\u003C{ created: number; updated: number }> {\n    let created = 0, updated = 0;\n    const PAGE_SIZE = 500;\n    let offset = 0;\n\n    while (true) {\n      const page = await this.client.post\u003C{ data: AttioRecord[] }>(\n        `\u002Fobjects\u002F${objectSlug}\u002Frecords\u002Fquery`,\n        { limit: PAGE_SIZE, offset }\n      );\n\n      for (const record of page.data) {\n        const existed = await this.upsertLocal(record);\n        existed ? updated++ : created++;\n      }\n\n      if (page.data.length \u003C PAGE_SIZE) break;\n      offset += PAGE_SIZE;\n    }\n\n    return { created, updated };\n  }\n}\n```\n\n### Component 4: Multi-Environment Config\n\n```typescript\n\u002F\u002F src\u002Fattio\u002Fconfig.ts\ninterface AttioEnvironmentConfig {\n  apiKey: string;\n  webhookSecret: string;\n  baseUrl: string;\n  cache: { schemaTtlMs: number; recordTtlMs: number };\n  rateLimit: { concurrency: number; intervalCap: number };\n}\n\nconst configs: Record\u003Cstring, Partial\u003CAttioEnvironmentConfig>> = {\n  development: {\n    cache: { schemaTtlMs: 60_000, recordTtlMs: 10_000 },\n    rateLimit: { concurrency: 2, intervalCap: 5 },\n  },\n  staging: {\n    cache: { schemaTtlMs: 300_000, recordTtlMs: 60_000 },\n    rateLimit: { concurrency: 5, intervalCap: 8 },\n  },\n  production: {\n    cache: { schemaTtlMs: 1_800_000, recordTtlMs: 300_000 },\n    rateLimit: { concurrency: 10, intervalCap: 15 },\n  },\n};\n\nexport function loadConfig(): AttioEnvironmentConfig {\n  const env = process.env.NODE_ENV || \"development\";\n  const envConfig = configs[env] || configs.development;\n  return {\n    apiKey: requireEnv(\"ATTIO_API_KEY\"),\n    webhookSecret: process.env.ATTIO_WEBHOOK_SECRET || \"\",\n    baseUrl: \"https:\u002F\u002Fapi.attio.com\u002Fv2\",\n    cache: envConfig.cache!,\n    rateLimit: envConfig.rateLimit!,\n  };\n}\n\nfunction requireEnv(key: string): string {\n  const val = process.env[key];\n  if (!val) throw new Error(`Missing required env: ${key}`);\n  return val;\n}\n```\n\n## Data Flow Diagram\n\n```\nExternal System                    Your Application                      Attio CRM\n     │                                   │                                  │\n     │  Local change ──────────────────▶ │                                  │\n     │                                   │  PUT \u002Fobjects\u002Fpeople\u002Frecords ──▶ │\n     │                                   │  ◀── 200 { data: record }       │\n     │                                   │                                  │\n     │                                   │       Webhook: record.updated    │\n     │                                   │  ◀──────────────────────────── │\n     │  ◀── Sync update ──────────────  │                                  │\n     │                                   │  GET \u002Fobjects\u002F...\u002Frecords\u002F... ─▶ │\n     │                                   │  ◀── 200 { data: record }       │\n```\n\n## Error Handling\n\n| Architecture issue | Symptom | Fix |\n|-------------------|---------|-----|\n| Service calls client directly | Tight coupling, hard to test | Add service layer facade |\n| No cache invalidation | Stale data after updates | Webhook-driven cache invalidation |\n| Sync conflicts | Both sides updated same record | Last-write-wins or conflict resolution queue |\n| No circuit breaker | Attio outage cascades | Add circuit breaker in Attio layer |\n\n## Resources\n\n- [Attio REST API Overview](https:\u002F\u002Fdocs.attio.com\u002Frest-api\u002Foverview)\n- [Attio Objects and Lists](https:\u002F\u002Fdocs.attio.com\u002Fdocs\u002Fobjects-and-lists)\n- [Attio Webhooks Guide](https:\u002F\u002Fdocs.attio.com\u002Frest-api\u002Fguides\u002Fwebhooks)\n- [Attio Developer Platform](https:\u002F\u002Fattio.com\u002Fplatform\u002Fdevelopers)\n\n## Next Steps\n\nThis is the capstone skill. For specific implementations, refer to the individual skills in this pack.\n\n",{"name":5,"tags":44,"author":46,"license":15,"version":47,"description":48,"allowed-tools":49,"compatible-with":50},[45,31,22],"saas","Jeremy Longshore \u003Cjeremy@intentsolutions.io>","1.0.0","Production reference architecture for Attio CRM integrations -- layered\nproject structure, sync patterns, webhook processing, and multi-environment setup.\nTrigger: \"attio architecture\", \"attio best practices\", \"attio project structure\",\n\"how to organize attio\", \"attio integration design\".\n","Read, Grep","claude-code","main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Fjeremylongshore\u002Fclaude-code-plugins-plus-skills\u002Fattio-reference-architecture",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":61,"prompt":71,"zipAvailable":72},[62,65,68],{"tool":63,"command":64},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill attio-reference-architecture",{"tool":66,"command":67},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill attio-reference-architecture",{"tool":69,"command":70},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill attio-reference-architecture","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fjeremylongshore\u002Fclaude-code-plugins-plus-skills\u002Fattio-reference-architecture\u002FSKILL.md",true,[],[75,89,99,108,114,121],{"skillNo":76,"name":77,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":85,"publishedAt":39},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[80],{"categoryNo":81,"categoryName":82,"taxonomyType":83,"taxonomyTypeDesc":84},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[86,87,88],"model","model hierarchy","model hierarchy skill",{"skillNo":90,"name":91,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":92,"categories":93,"keywords":95,"publishedAt":39},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[94],{"categoryNo":81,"categoryName":82,"taxonomyType":83,"taxonomyTypeDesc":84},[96,97,98],"MCP","zoom","zoom mcp",{"skillNo":100,"name":101,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":102,"categories":103,"keywords":104,"publishedAt":39},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[105,106,107,97],"sdk","zoom apps sdk","apps",{"skillNo":109,"name":110,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":111,"categories":112,"keywords":113,"publishedAt":39},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[110],{"skillNo":115,"name":116,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":117,"categories":118,"keywords":120,"publishedAt":39},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[119],{"categoryNo":81,"categoryName":82,"taxonomyType":83,"taxonomyTypeDesc":84},[116],{"skillNo":122,"name":123,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":124,"categories":125,"keywords":126,"publishedAt":39},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[123]]