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
-- 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
// 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<SyncResult> {
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<SyncResult> {
const lastSync = await db.getLastSyncTimestamp();
const changed = await this.client.request<any>(
'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<string, string>) {
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
// src/services/lifecycle.ts
export class EmployeeLifecycleService {
constructor(
private bamboohr: BambooHRClient,
private slackClient: any,
private googleAdmin: any,
) {}
async onNewEmployee(employeeId: string, fields: Record<string, string>) {
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<string, string>) {
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<string, string>) {
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
// 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
// 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
Flagship Skills
For the complete BambooHR skill pack, start with bamboohr-install-auth.