Architecting a Mono-Repo Strategy for Managing Multiple Genesys Cloud Integration Projects
What This Guide Covers
You are consolidating your organization’s Genesys Cloud integration codebases - currently spread across 15+ separate Git repositories for Data Actions, Architect flow exports, Client App SDK widgets, Lambda functions, and Terraform infrastructure - into a single mono-repo. When complete, all Genesys Cloud integration code lives in one repository with shared tooling, a unified CI/CD pipeline, cross-project dependency management, and atomic commits that update both a Data Action definition and the Lambda function it calls in a single pull request. This replaces the current model where a change to a shared utility requires PRs across 4 repositories, manual version bumping, and coordinated deployments that frequently drift out of sync.
Prerequisites, Roles & Licensing
- Tooling:
- Git (with Git LFS for Architect flow exports if needed)
- A mono-repo build tool: Turborepo, Nx, or Lerna (this guide uses Turborepo)
- Node.js 20+ for workspace management
- CI/CD: GitHub Actions (or equivalent)
- Team requirements: All integration developers agree on shared linting, testing, and deployment conventions.
The Implementation Deep-Dive
1. Repository Structure
genesys-integrations/
├── package.json # Root workspace config (Turborepo)
├── turbo.json # Turborepo pipeline configuration
├── .github/workflows/ # Shared CI/CD pipelines
│
├── packages/ # Shared libraries
│ ├── gc-auth/ # Genesys Cloud OAuth client wrapper
│ │ ├── package.json
│ │ ├── src/index.ts
│ │ └── tsconfig.json
│ ├── gc-api-client/ # Typed API client for Genesys Cloud
│ │ ├── package.json
│ │ └── src/
│ ├── gc-test-utils/ # Shared test fixtures and mocks
│ │ ├── package.json
│ │ └── src/
│ └── gc-logger/ # Structured logging library
│ ├── package.json
│ └── src/
│
├── apps/ # Deployable applications
│ ├── crm-sidebar-widget/ # Client App SDK widget
│ │ ├── package.json
│ │ ├── src/
│ │ └── vite.config.ts
│ ├── agent-coaching-widget/ # Another Client App SDK widget
│ │ ├── package.json
│ │ └── src/
│ ├── workforce-dashboard/ # Supervisor WFM dashboard
│ │ ├── package.json
│ │ └── src/
│ └── webhook-receiver/ # Express server for Genesys webhooks
│ ├── package.json
│ └── src/
│
├── functions/ # AWS Lambda / Cloud Functions
│ ├── data-action-crm-lookup/
│ │ ├── package.json
│ │ ├── handler.ts
│ │ └── serverless.yml
│ ├── data-action-order-status/
│ │ ├── package.json
│ │ └── handler.ts
│ ├── event-bridge-processor/
│ │ ├── package.json
│ │ └── handler.ts
│ └── scheduled-report-exporter/
│ ├── package.json
│ └── handler.ts
│
├── infrastructure/ # Terraform / IaC
│ ├── modules/
│ │ ├── genesys-oauth-client/
│ │ ├── lambda-data-action/
│ │ └── api-gateway/
│ ├── environments/
│ │ ├── dev/
│ │ ├── staging/
│ │ └── prod/
│ └── main.tf
│
└── config/ # Shared configuration
├── architect-flows/ # Exported Architect flow YAML/JSON
├── data-actions/ # Data Action JSON definitions
└── routing/ # Queue, skill, wrap-up code configs
2. Root Workspace Configuration
// package.json (root)
{
"name": "genesys-integrations",
"private": true,
"workspaces": [
"packages/*",
"apps/*",
"functions/*"
],
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"deploy:dev": "turbo run deploy --filter='...[origin/main]' -- --stage=dev",
"deploy:prod": "turbo run deploy --filter='...[origin/main]' -- --stage=prod",
"typecheck": "turbo run typecheck"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.4.0",
"eslint": "^9.0.0",
"prettier": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^7.0.0"
}
}
3. Turborepo Pipeline (Incremental Builds)
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["tsconfig.base.json"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "build/**"],
"cache": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"outputs": [],
"cache": true
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": [],
"cache": true
},
"deploy": {
"dependsOn": ["build", "test", "lint"],
"outputs": [],
"cache": false
}
}
}
The key feature: turbo run build --filter='...[origin/main]' only builds packages/apps that changed since main - a 15-project mono-repo doesn’t rebuild everything on every commit.
4. Shared Genesys Cloud Auth Package
// packages/gc-auth/src/index.ts
import axios from 'axios';
export interface GCAuthConfig {
clientId: string;
clientSecret: string;
region?: 'us-east-1' | 'eu-west-1' | 'ap-southeast-2' | 'ap-northeast-1';
}
const REGION_URLS: Record<string, string> = {
'us-east-1': 'https://login.mypurecloud.com',
'eu-west-1': 'https://login.mypurecloud.ie',
'ap-southeast-2': 'https://login.mypurecloud.com.au',
'ap-northeast-1': 'https://login.mypurecloud.jp',
};
export class GCAuth {
private token: string | null = null;
private expiresAt: number = 0;
constructor(private config: GCAuthConfig) {}
async getToken(): Promise<string> {
if (this.token && Date.now() < this.expiresAt - 60000) {
return this.token;
}
const loginUrl = REGION_URLS[this.config.region || 'us-east-1'];
const resp = await axios.post(
`${loginUrl}/oauth/token`,
'grant_type=client_credentials',
{
auth: { username: this.config.clientId, password: this.config.clientSecret },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
);
this.token = resp.data.access_token;
this.expiresAt = Date.now() + (resp.data.expires_in * 1000);
return this.token!;
}
}
Every function and app imports @genesys-integrations/gc-auth rather than copy-pasting OAuth logic:
// functions/data-action-crm-lookup/handler.ts
import { GCAuth } from '@genesys-integrations/gc-auth';
const auth = new GCAuth({
clientId: process.env.GC_CLIENT_ID!,
clientSecret: process.env.GC_CLIENT_SECRET!,
region: 'us-east-1'
});
export async function handler(event: any) {
const token = await auth.getToken();
// Use token for Genesys API calls...
}
5. Affected-Project-Only CI/CD
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
affected: ${{ steps.turbo.outputs.affected }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: npm ci
- id: turbo
run: |
AFFECTED=$(npx turbo run build --dry-run=json --filter='...[origin/main]' | jq -r '.packages | join(",")')
echo "affected=$AFFECTED" >> $GITHUB_OUTPUT
build-and-test:
needs: detect-changes
if: needs.detect-changes.outputs.affected != ''
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx turbo run build test lint --filter='...[origin/main]'
Validation, Edge Cases & Troubleshooting
Edge Case 1: Architect Flow Exports Are Large Binary Files
Exported Architect flow files can be 5-50 MB of JSON. Git becomes slow with hundreds of large flow versions in history.
Solution: Use Git LFS for the config/architect-flows/ directory. Add to .gitattributes: config/architect-flows/**/*.json filter=lfs diff=lfs merge=lfs -text. This keeps the Git history small while still versioning flow exports.
Edge Case 2: A Shared Package Change Triggers All 15 Projects to Rebuild
Updating gc-auth (used by every function and app) causes turbo run build to rebuild everything, even if the change was just a comment fix.
Solution: Turborepo’s content-hash-based caching handles this correctly - if the compiled output of gc-auth doesn’t change (same hash), downstream projects use their cached builds. Ensure outputs in turbo.json are correctly configured so Turborepo can compare hashes. For truly breaking changes, use the --concurrency flag to parallelize the rebuild.
Edge Case 3: Different Lambda Functions Need Different Node.js Runtimes
Your data-action-crm-lookup runs on Node 18 but event-bridge-processor requires Node 20 for a specific dependency.
Solution: Each function’s serverless.yml (or equivalent deployment config) specifies its own runtime. The mono-repo’s root TypeScript config (tsconfig.base.json) targets the lowest common denominator (ES2022), and each function extends it with its own tsconfig.json that can override the target. The build step for each function produces a standalone bundle (via esbuild or tsup) that’s runtime-agnostic.