Claude-code-plugins-plus-skills apollo-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/apollo-pack/skills/apollo-reference-architecture" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-apollo-reference-architecture && rm -rf "$T"
manifest:
plugins/saas-packs/apollo-pack/skills/apollo-reference-architecture/SKILL.mdsource content
Apollo Reference Architecture
Overview
Production-ready reference architecture for Apollo.io integrations. Layered design with API client, service layer, background jobs, database models, CRM sync, and deals pipeline — all built around Apollo's REST API with correct endpoints and
x-api-key authentication.
Prerequisites
- Apollo master API key
- Node.js 18+ with TypeScript
- PostgreSQL for data layer
- Redis for job queues
Instructions
Step 1: Architecture Diagram
┌───────────────────────────────────────────────┐ │ API Layer │ Express routes │ POST /api/leads/search GET /api/org/:d │ POST /api/deals ├───────────────────────────────────────────────┤ │ Service Layer │ Business logic │ LeadService EnrichService DealService │ SequenceService ├───────────────────────────────────────────────┤ │ Client Layer │ Apollo API wrapper │ ApolloClient RateLimiter Cache │ CreditTracker ├───────────────────────────────────────────────┤ │ Background Jobs │ BullMQ queues │ EnrichJob SyncJob StageChangeJob │ TaskCreatorJob ├───────────────────────────────────────────────┤ │ Data Layer │ Prisma/TypeORM │ Contact Organization Deal AuditLog │ └───────────────────────────────────────────────┘
Step 2: Service Layer
// src/services/lead-service.ts import { getApolloClient } from '../apollo/client'; import { withRetry } from '../apollo/retry'; import { cachedRequest } from '../apollo/cache'; export class LeadService { private client = getApolloClient(); async searchPeople(params: { domains: string[]; titles?: string[]; seniorities?: string[]; page?: number }) { return cachedRequest('/mixed_people/api_search', () => withRetry(() => this.client.post('/mixed_people/api_search', { q_organization_domains_list: params.domains, person_titles: params.titles, person_seniorities: params.seniorities, page: params.page ?? 1, per_page: 100, })), params, ); } async enrichPerson(email: string) { return withRetry(() => this.client.post('/people/match', { email })); } async enrichOrg(domain: string) { return cachedRequest('/organizations/enrich', () => withRetry(() => this.client.get('/organizations/enrich', { params: { domain } })), { domain }, ); } }
Step 3: Deals/Opportunities Service
Apollo has a full Deals API for tracking revenue pipeline.
// src/services/deal-service.ts export class DealService { private client = getApolloClient(); async createDeal(params: { name: string; amount: number; ownerId: string; // Apollo user ID accountId?: string; // Apollo account ID contactIds?: string[]; // Apollo contact IDs stageId?: string; // Deal stage ID }) { const { data } = await this.client.post('/opportunities', { name: params.name, amount: params.amount, owner_id: params.ownerId, account_id: params.accountId, contact_ids: params.contactIds, opportunity_stage_id: params.stageId, }); return { dealId: data.opportunity.id, name: data.opportunity.name }; } async listDeals(page: number = 1) { const { data } = await this.client.post('/opportunities/search', { page, per_page: 50 }); return data.opportunities.map((d: any) => ({ id: d.id, name: d.name, amount: d.amount, stage: d.opportunity_stage?.name, owner: d.owner?.name, })); } async getDealStages() { const { data } = await this.client.get('/opportunity_stages'); return data.opportunity_stages.map((s: any) => ({ id: s.id, name: s.name, order: s.display_order })); } async updateDeal(dealId: string, updates: { amount?: number; stageId?: string }) { await this.client.patch(`/opportunities/${dealId}`, { amount: updates.amount, opportunity_stage_id: updates.stageId, }); } }
Step 4: Background Job Processing
// src/jobs/enrichment-job.ts import { Queue, Worker, Job } from 'bullmq'; import { LeadService } from '../services/lead-service'; const connection = { host: process.env.REDIS_HOST ?? 'localhost', port: 6379 }; export const enrichmentQueue = new Queue('apollo-enrichment', { connection, defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 5000 }, removeOnComplete: 1000, }, }); const leadService = new LeadService(); new Worker('apollo-enrichment', async (job: Job) => { switch (job.name) { case 'enrich-person': return leadService.enrichPerson(job.data.email); case 'enrich-org': return leadService.enrichOrg(job.data.domain); case 'bulk-search': { const results: any[] = []; for (const domain of job.data.domains) { const { data } = await leadService.searchPeople({ domains: [domain] }); results.push(...data.people); await job.updateProgress(results.length); } return { total: results.length }; } } }, { connection, concurrency: 3, limiter: { max: 50, duration: 60_000 } });
Step 5: Database Model
// src/models/contact.ts (Prisma schema excerpt) // model Contact { // id String @id @default(cuid()) // apolloId String @unique // email String @unique // name String // title String? // seniority String? // phone String? // linkedinUrl String? // organizationId String? // rawApolloData Json? // enrichedAt DateTime? // createdAt DateTime @default(now()) // updatedAt DateTime @updatedAt // } // TypeORM version import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; @Entity('contacts') export class Contact { @PrimaryColumn() apolloId: string; @Column({ unique: true }) email: string; @Column() name: string; @Column({ nullable: true }) title: string; @Column({ nullable: true }) seniority: string; @Column({ nullable: true }) phone: string; @Column({ nullable: true }) linkedinUrl: string; @Column({ type: 'jsonb', nullable: true }) rawApolloData: Record<string, any>; @Column({ nullable: true }) enrichedAt: Date; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; }
Step 6: API Routes
// src/api/routes.ts import { Router } from 'express'; import { LeadService } from '../services/lead-service'; import { DealService } from '../services/deal-service'; const router = Router(); const leads = new LeadService(); const deals = new DealService(); router.post('/api/leads/search', async (req, res) => { const { data } = await leads.searchPeople(req.body); res.json({ leads: data.people, pagination: data.pagination }); }); router.post('/api/leads/enrich', async (req, res) => { const { data } = await leads.enrichPerson(req.body.email); res.json({ contact: data.person }); }); router.get('/api/organizations/:domain', async (req, res) => { const { data } = await leads.enrichOrg(req.params.domain); res.json({ organization: data.organization }); }); router.post('/api/deals', async (req, res) => { const result = await deals.createDeal(req.body); res.json(result); }); router.get('/api/deals', async (req, res) => { const list = await deals.listDeals(parseInt(req.query.page as string) || 1); res.json({ deals: list }); }); export { router };
Output
- Layered architecture: API, Service, Client, Jobs, Data
with cached search and retried enrichmentLeadService
with create, list, update, and stage managementDealService- BullMQ background jobs for async enrichment
- Database model (Prisma + TypeORM)
- Express API routes for search, enrichment, and deals
Error Handling
| Layer | Strategy |
|---|---|
| Client | Retry with backoff, circuit breaker for prolonged outages |
| Service | Cache fallback on failure, credit budget enforcement |
| Jobs | 3 retries with exponential backoff, dead letter after max |
| API | Structured JSON error responses with error codes |
Resources
Next Steps
Proceed to
apollo-multi-env-setup for environment configuration.