--- name: bamboohr-reference-architecture description: 'Implement BambooHR reference architecture for production HR data pipelines. Use when designing new BambooHR integrations, building employee sync systems, or establishing architecture standards for BambooHR-powered applications. Trigger with phrases like "bamboohr architecture", "bamboohr design", "bamboohr project structure", "bamboohr system design", "bamboohr pipeline". ' allowed-tools: Read, Grep version: 1.0.0 license: MIT author: Jeremy Longshore tags: - saas - hr - bamboohr - architecture compatibility: Designed for Claude Code --- # BambooHR Reference Architecture ## Overview Production-ready architecture for BambooHR integrations covering the three most common patterns: real-time employee sync, HR data pipeline, and employee lifecycle automation. ## Prerequisites - Understanding of layered architecture and event-driven design - BambooHR API knowledge from earlier skills in this pack - TypeScript project setup with Node.js 18+ ## Instructions ### Architecture Overview ``` ┌──────────────────────────────────────────────────────────┐ │ Your Application │ ├──────────────┬───────────────┬────────────────────────────┤ │ API Layer │ Sync Engine │ Webhook Handler │ │ /api/* │ (Cron/Queue) │ /webhooks/bamboohr │ ├──────────────┴───────────────┴────────────────────────────┤ │ Service Layer │ │ EmployeeService │ TimeOffService │ ReportService │ ├───────────────────┴──────────────────┴────────────────────┤ │ BambooHR Client Layer │ │ BambooHRClient │ Cache │ RetryHandler │ Metrics │ ├───────────────────┴─────────┴────────────────┴────────────┤ │ Data Layer │ │ PostgreSQL (employees) │ Redis (cache) │ S3 (files) │ └───────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────┐ │ BambooHR REST API │ │ api.bamboohr.com/api/ │ │ gateway.php/{co}/v1 │ └────────────────────────┘ ``` ### Project Structure ``` bamboohr-integration/ ├── src/ │ ├── bamboohr/ │ │ ├── client.ts # HTTP client (from sdk-patterns) │ │ ├── types.ts # BambooHR API response types │ │ ├── retry.ts # Retry with Retry-After support │ │ ├── cache.ts # LRU + Redis cache layer │ │ └── metrics.ts # Request counting and latency │ ├── services/ │ │ ├── employee-sync.ts # Incremental directory sync │ │ ├── time-off.ts # PTO balance and request management │ │ ├── reports.ts # Custom report generation │ │ └── lifecycle.ts # Onboarding/offboarding automation │ ├── handlers/ │ │ ├── webhook.ts # Webhook signature verification + routing │ │ └── events.ts # Employee change event processors │ ├── api/ │ │ ├── health.ts # Health check endpoint │ │ ├── employees.ts # REST API for local employee data │ │ └── reports.ts # Report generation endpoints │ ├── jobs/ │ │ ├── full-sync.ts # Scheduled full directory sync │ │ ├── incremental-sync.ts # Frequent delta sync │ │ └── report-export.ts # Scheduled report export │ └── db/ │ ├── schema.sql # PostgreSQL schema │ └── queries.ts # Database queries ├── tests/ │ ├── unit/ │ │ ├── client.test.ts │ │ ├── employee-sync.test.ts │ │ └── webhook.test.ts │ ├── integration/ │ │ └── bamboohr-live.test.ts │ └── mocks/ │ └── bamboohr-handlers.ts # MSW handlers ├── config/ │ ├── default.json │ ├── production.json │ └── test.json └── docker-compose.yml # PostgreSQL + Redis for local dev ``` ### Step 1: Data Model ```sql -- db/schema.sql CREATE TABLE bamboohr_employees ( id INTEGER PRIMARY KEY, -- BambooHR employee ID first_name TEXT NOT NULL, last_name TEXT NOT NULL, display_name TEXT, work_email TEXT, job_title TEXT, department TEXT, division TEXT, location TEXT, supervisor_id INTEGER REFERENCES bamboohr_employees(id), status TEXT DEFAULT 'Active', hire_date DATE, termination_date DATE, employee_number TEXT, raw_data JSONB, -- Full BambooHR response synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_employees_status ON bamboohr_employees(status); CREATE INDEX idx_employees_department ON bamboohr_employees(department); CREATE INDEX idx_employees_synced ON bamboohr_employees(synced_at); CREATE TABLE bamboohr_sync_log ( id SERIAL PRIMARY KEY, sync_type TEXT NOT NULL, -- 'full', 'incremental', 'webhook' started_at TIMESTAMPTZ NOT NULL, completed_at TIMESTAMPTZ, employees_created INTEGER DEFAULT 0, employees_updated INTEGER DEFAULT 0, employees_deleted INTEGER DEFAULT 0, errors JSONB DEFAULT '[]', status TEXT DEFAULT 'running' -- 'running', 'completed', 'failed' ); ``` ### Step 2: Employee Sync Service ```typescript // src/services/employee-sync.ts import { BambooHRClient } from '../bamboohr/client'; import { db } from '../db/queries'; const SYNC_FIELDS = [ 'firstName', 'lastName', 'displayName', 'workEmail', 'jobTitle', 'department', 'division', 'location', 'supervisor', 'status', 'hireDate', 'terminationDate', 'employeeNumber', ]; export class EmployeeSyncService { constructor(private client: BambooHRClient) {} async fullSync(): Promise { const log = await db.createSyncLog('full'); try { // One API call for all employee data const report = await this.client.customReport(SYNC_FIELDS); const result = { created: 0, updated: 0, deleted: 0, errors: [] as string[] }; for (const emp of report.employees) { try { const existing = await db.getEmployee(parseInt(emp.id)); if (existing) { await db.updateEmployee(parseInt(emp.id), emp); result.updated++; } else { await db.createEmployee(parseInt(emp.id), emp); result.created++; } } catch (err) { result.errors.push(`Employee ${emp.id}: ${(err as Error).message}`); } } // Mark employees not in report as inactive const activeIds = new Set(report.employees.map(e => parseInt(e.id))); const localEmployees = await db.getActiveEmployeeIds(); for (const localId of localEmployees) { if (!activeIds.has(localId)) { await db.deactivateEmployee(localId); result.deleted++; } } await db.completeSyncLog(log.id, result); return result; } catch (err) { await db.failSyncLog(log.id, (err as Error).message); throw err; } } async incrementalSync(): Promise { const lastSync = await db.getLastSyncTimestamp(); const changed = await this.client.request( 'GET', `/employees/changed/?since=${lastSync}`, ); const changedIds = Object.keys(changed.employees || {}); if (changedIds.length === 0) return { created: 0, updated: 0, deleted: 0, errors: [] }; // Fetch details for changed employees only const result = { created: 0, updated: 0, deleted: 0, errors: [] as string[] }; for (const id of changedIds) { const emp = await this.client.getEmployee(id, SYNC_FIELDS); // Upsert logic... } return result; } async handleWebhookEvent(employeeId: string, action: string, fields: Record) { switch (action) { case 'Created': await db.createEmployee(parseInt(employeeId), fields); break; case 'Updated': await db.updateEmployee(parseInt(employeeId), fields); break; case 'Deleted': await db.deactivateEmployee(parseInt(employeeId)); break; } } } ``` ### Step 3: Employee Lifecycle Automation ```typescript // src/services/lifecycle.ts export class EmployeeLifecycleService { constructor( private bamboohr: BambooHRClient, private slackClient: any, private googleAdmin: any, ) {} async onNewEmployee(employeeId: string, fields: Record) { const { firstName, lastName, workEmail, department, jobTitle, supervisor } = fields; // 1. Create Google Workspace account await this.googleAdmin.createUser({ primaryEmail: workEmail, name: { givenName: firstName, familyName: lastName }, orgUnitPath: `/departments/${department}`, }); // 2. Add to Slack await this.slackClient.inviteUser(workEmail); await this.slackClient.addToChannel(workEmail, `#${department.toLowerCase()}`); // 3. Notify manager await this.slackClient.sendDM(supervisor, { text: `Your new report ${firstName} ${lastName} (${jobTitle}) starts soon. ` + `BambooHR profile: https://${process.env.BAMBOOHR_COMPANY_DOMAIN}.bamboohr.com/employees/employee.php?id=${employeeId}`, }); console.log(`Onboarding complete for ${firstName} ${lastName}`); } async onEmployeeTerminated(employeeId: string, fields: Record) { const { workEmail, firstName, lastName } = fields; // 1. Disable Google Workspace account await this.googleAdmin.suspendUser(workEmail); // 2. Deactivate Slack await this.slackClient.deactivateUser(workEmail); // 3. Archive in downstream systems console.log(`Offboarding complete for ${firstName} ${lastName}`); } async onDepartmentChanged(employeeId: string, fields: Record) { const { workEmail, department } = fields; // Update Google Workspace OU await this.googleAdmin.moveUser(workEmail, `/departments/${department}`); // Update Slack channels await this.slackClient.addToChannel(workEmail, `#${department.toLowerCase()}`); } } ``` ### Step 4: Sync Scheduling ```typescript // src/jobs/sync-scheduler.ts import cron from 'node-cron'; // Full sync: daily at 2 AM cron.schedule('0 2 * * *', async () => { console.log('Starting full BambooHR sync...'); const result = await syncService.fullSync(); console.log(`Full sync: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted`); }); // Incremental sync: every 15 minutes (safety net for missed webhooks) cron.schedule('*/15 * * * *', async () => { const result = await syncService.incrementalSync(); if (result.created + result.updated + result.deleted > 0) { console.log(`Incremental sync: ${JSON.stringify(result)}`); } }); ``` ### Step 5: Configuration Management ```typescript // config/bamboohr.ts export interface BambooHRIntegrationConfig { api: { companyDomain: string; apiKey: string; timeoutMs: number; maxRetries: number; }; sync: { fullSyncCron: string; incrementalSyncCron: string; batchSize: number; }; cache: { directoryTtlMs: number; employeeTtlMs: number; redisUrl?: string; }; webhook: { secret: string; path: string; replayWindowMs: number; }; } export function loadConfig(): BambooHRIntegrationConfig { return { api: { companyDomain: process.env.BAMBOOHR_COMPANY_DOMAIN!, apiKey: process.env.BAMBOOHR_API_KEY!, timeoutMs: 30_000, maxRetries: 3, }, sync: { fullSyncCron: '0 2 * * *', incrementalSyncCron: '*/15 * * * *', batchSize: 100, }, cache: { directoryTtlMs: 5 * 60 * 1000, employeeTtlMs: 60 * 1000, redisUrl: process.env.REDIS_URL, }, webhook: { secret: process.env.BAMBOOHR_WEBHOOK_SECRET!, path: '/webhooks/bamboohr', replayWindowMs: 300_000, }, }; } ``` ## Output - Layered architecture separating API, service, client, and data concerns - Employee sync with full, incremental, and webhook-driven modes - PostgreSQL schema for local employee data with audit trail - Lifecycle automation (onboarding, offboarding, department changes) - Scheduled sync jobs with cron - Environment-specific configuration ## Error Handling | Issue | Cause | Solution | |-------|-------|----------| | Sync data gaps | Missed webhooks + long incremental interval | Full sync as daily safety net | | Duplicate processing | Webhook retry + no idempotency | Idempotency keys in sync log | | Stale local data | Cache TTL too long | Webhook-based cache invalidation | | Circular dependencies | Poor layer separation | Strict dependency direction (API > Service > Client > DB) | ## Enterprise Considerations - **Multi-company**: Deploy separate instances per BambooHR company domain, or use tenant-aware client factory - **Data residency**: Store employee data in the same region as your BambooHR instance - **Compliance**: Implement data retention policies; BambooHR data includes PII - **High availability**: Run sync workers as separate pods/containers from API servers - **Monitoring**: Alert on sync failures, webhook delivery gaps, and API error spikes ## Resources - [BambooHR API Documentation](https://documentation.bamboohr.com/docs) - [BambooHR API Reference](https://documentation.bamboohr.com/reference) - [BambooHR Webhooks](https://documentation.bamboohr.com/docs/webhooks) ## Flagship Skills For the complete BambooHR skill pack, start with `bamboohr-install-auth`.