| name | attio-reference-architecture |
| description | Production reference architecture for Attio CRM integrations -- layered
project structure, sync patterns, webhook processing, and multi-environment setup.
Trigger: "attio architecture", "attio best practices", "attio project structure",
"how to organize attio", "attio integration design".
|
| allowed-tools | Read, Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","crm","attio"] |
| compatible-with | claude-code |
Attio Reference Architecture
Overview
Production architecture for applications that integrate with the Attio REST API (https://api.attio.com/v2). Covers project layout, layered service design, sync patterns, and operational concerns.
Project Structure
my-attio-integration/
├── src/
│ ├── attio/ # Attio API layer (isolated)
│ │ ├── client.ts # Typed fetch wrapper with retry
│ │ ├── types.ts # Attio API types (AttioRecord, AttioError, etc.)
│ │ ├── config.ts # Environment-based config loader
│ │ └── errors.ts # AttioApiError class
│ ├── services/ # Business logic (uses attio/ layer)
│ │ ├── contacts.ts # People/company sync logic
│ │ ├── pipeline.ts # Deal pipeline management
│ │ ├── activity.ts # Notes, tasks, comments
│ │ └── sync.ts # Bi-directional sync orchestrator
│ ├── webhooks/ # Incoming webhook handlers
│ │ ├── router.ts # Event type routing
│ │ ├── verify.ts # Signature verification
│ │ └── handlers/
│ │ ├── record-events.ts # record.created/updated/deleted/merged
│ │ ├── entry-events.ts # list-entry.created/updated/deleted
│ │ └── activity-events.ts # note/task/comment events
│ ├── api/ # Outbound API routes
│ │ ├── health.ts # Health check (includes Attio)
│ │ └── webhooks.ts # Webhook receiver endpoint
│ ├── cache/ # Caching layer
│ │ ├── schema-cache.ts # Object/attribute definitions (30min TTL)
│ │ └── record-cache.ts # Record data (5min TTL, webhook invalidation)
│ └── index.ts # App entrypoint
├── tests/
│ ├── mocks/ # MSW handlers for Attio API
│ ├── unit/ # Service logic tests (mocked API)
│ └── integration/ # Live API tests (CI-gated)
├── config/
│ ├── attio.development.json
│ ├── attio.staging.json
│ └── attio.production.json
├── .env.example
└── .github/workflows/attio.yml
Layered Architecture
┌──────────────────────────────────────────────────┐
│ API Layer (routes, webhook endpoint) │
│ - Receives HTTP requests │
│ - Validates webhook signatures │
│ - Returns health status │
├──────────────────────────────────────────────────┤
│ Service Layer (business logic) │
│ - Contact sync, pipeline management │
│ - Bi-directional data mapping │
│ - Event-driven automations │
├──────────────────────────────────────────────────┤
│ Attio Layer (API client, types, errors) │
│ - Typed fetch wrapper with retry │
│ - Error normalization (AttioApiError) │
│ - Pagination helpers │
├──────────────────────────────────────────────────┤
│ Infrastructure Layer (cache, queue, monitoring) │
│ - LRU + Redis caching with webhook invalidation │
│ - Rate limit queue (p-queue) │
│ - Structured logging and metrics │
└──────────────────────────────────────────────────┘
Rule: Each layer only calls the layer directly below it. The API layer never calls the Attio client directly.
Core Components
Component 1: Service Layer Facade
import { AttioClient } from "../attio/client";
import { cachedGet, invalidateRecord } from "../cache/record-cache";
import type { AttioRecord } from "../attio/types";
export class ContactService {
constructor(private client: AttioClient) {}
async findByEmail(email: string): Promise<AttioRecord | null> {
const res = await this.client.post<{ data: AttioRecord[] }>(
"/objects/people/records/query",
{
filter: { email_addresses: email },
limit: 1,
}
);
return res.data[0] || null;
}
async upsertPerson(data: {
email: string;
firstName: string;
lastName: ;
?: ;
}): <> {
res = ..<{ : }>(
,
{
: {
: {
: [data.],
: [{
: data.,
: data.,
: ,
}],
...(data. ? { : [{ : , : data. }] } : {}),
},
},
}
);
res.;
}
(
: ,
: ,
: ,
?: { : ; : }
): <> {
..(, {
: {
: recordId,
: ,
: {
: [{ : stage }],
...(value ? {
: [{ : value., : value. }],
} : {}),
},
},
});
}
(: , : , : ): <> {
..(, {
: {
: ,
: recordId,
title,
: ,
content,
},
});
}
}
Component 2: Webhook Event Router
import type { AttioWebhookEvent } from "../attio/types";
type EventHandler = (event: AttioWebhookEvent) => Promise<void>;
export class WebhookRouter {
private handlers = new Map<string, EventHandler[]>();
on(eventType: string, handler: EventHandler): void {
const existing = this.handlers.get(eventType) || [];
this.handlers.set(eventType, [...existing, handler]);
}
async route(event: AttioWebhookEvent): Promise<void> {
const handlers = this.handlers.get(event.event_type) || [];
if (handlers.length === 0) {
console.log();
;
}
.(handlers.( (event)));
}
}
router = ();
router.(, (event) => {
(event.?. === ) {
(event.!..);
}
});
router.(, (event) => {
(event.!..);
});
router.(, (event) => {
(event);
});
Component 3: Bi-Directional Sync
export class AttioSyncService {
private lastSyncCursor: string | null = null;
async pushToAttio(localContact: LocalContact): Promise<string> {
const attioRecord = await this.contacts.upsertPerson({
email: localContact.email,
firstName: localContact.firstName,
lastName: localContact.lastName,
});
return attioRecord.id.record_id;
}
async handleAttioChange(event: AttioWebhookEvent): Promise<void> {
if (event.event_type === "record.updated") {
const record = await this.client.get<{ data: AttioRecord }>(
`/objects/${event.object!.api_slug}/records/`
);
.(record.);
}
}
(: ): <{ : ; : }> {
created = , updated = ;
= ;
offset = ;
() {
page = ..<{ : [] }>(
,
{ : , offset }
);
( record page.) {
existed = .(record);
existed ? updated++ : created++;
}
(page.. < ) ;
offset += ;
}
{ created, updated };
}
}
Component 4: Multi-Environment Config
interface AttioEnvironmentConfig {
apiKey: string;
webhookSecret: string;
baseUrl: string;
cache: { schemaTtlMs: number; recordTtlMs: number };
rateLimit: { concurrency: number; intervalCap: number };
}
const configs: Record<string, Partial<AttioEnvironmentConfig>> = {
development: {
cache: { schemaTtlMs: 60_000, recordTtlMs: 10_000 },
rateLimit: { concurrency: 2, intervalCap: 5 },
},
staging: {
cache: { schemaTtlMs: 300_000, recordTtlMs: 60_000 },
rateLimit: { concurrency: 5, intervalCap: 8 },
},
production: {
cache: { schemaTtlMs: 1_800_000, recordTtlMs: 300_000 },
rateLimit: { : , : },
},
};
(): {
env = process.. || ;
envConfig = configs[env] || configs.;
{
: (),
: process.. || ,
: ,
: envConfig.!,
: envConfig.!,
};
}
(): {
val = process.[key];
(!val) ();
val;
}
Data Flow Diagram
External System Your Application Attio CRM
│ │ │
│ Local change ──────────────────▶ │ │
│ │ PUT /objects/people/records ──▶ │
│ │ ◀── 200 { data: record } │
│ │ │
│ │ Webhook: record.updated │
│ │ ◀──────────────────────────── │
│ ◀── Sync update ────────────── │ │
│ │ GET /objects/.../records/... ─▶ │
│ │ ◀── 200 { data: record } │
Error Handling
| Architecture issue | Symptom | Fix |
|---|
| Service calls client directly | Tight coupling, hard to test | Add service layer facade |
| No cache invalidation | Stale data after updates | Webhook-driven cache invalidation |
| Sync conflicts | Both sides updated same record | Last-write-wins or conflict resolution queue |
| No circuit breaker | Attio outage cascades | Add circuit breaker in Attio layer |
Resources
Next Steps
This is the capstone skill. For specific implementations, refer to the individual skills in this pack.