/healthcare
/white-label
/apps
/web-core
/web-enterprise
```
3. Implement Variant Composition:
Variants should be built by extending core configurations. Use a factory pattern to assemble the application based on the target variant. This allows build-time optimization and runtime isolation.
```typescript
// packages/core/src/variant/types.ts
export interface VariantDefinition {
id: string;
name: string;
features: Record<string, boolean>;
configOverrides: Partial<AppConfig>;
assetManifest: AssetManifest;
}
export interface AppConfig {
apiBaseUrl: string;
theme: ThemeConfig;
featureFlags: FeatureFlagSet;
compliance: ComplianceConfig;
}
```
```typescript
// packages/variants/enterprise/src/config.ts
import { VariantDefinition, AppConfig } from '@codcompass/core/variant';
export const enterpriseVariant: VariantDefinition = {
id: 'enterprise',
name: 'Enterprise Suite',
features: {
sso: true,
auditLogs: true,
customRoles: true,
},
configOverrides: {
theme: { primaryColor: '#003366' },
compliance: { gdpr: true, hipaa: false },
},
assetManifest: {
logo: '/assets/enterprise-logo.svg',
favicon: '/assets/enterprise-favicon.ico',
},
};
// packages/core/src/variant/composer.ts
export function composeConfig(
baseConfig: AppConfig,
variant: VariantDefinition
): AppConfig {
return {
...baseConfig,
...variant.configOverrides,
featureFlags: {
...baseConfig.featureFlags,
...variant.features,
},
};
}
```
4. Database Schema Strategy:
Database variability must be managed carefully. Use a shared schema with extension tables approach. Core tables remain immutable across variants. Variant-specific data resides in extension tables linked via foreign keys. This prevents schema drift and simplifies migrations.
```sql
-- Core schema
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Enterprise variant extension
CREATE TABLE enterprise_user_attributes (
user_id UUID PRIMARY KEY REFERENCES users(id),
sso_provider VARCHAR(50),
sso_subject VARCHAR(255),
department VARCHAR(100)
);
```
5. CI/CD Matrix Configuration:
Implement a build matrix that compiles and tests each variant independently. Shared tests run once against the core, while variant-specific tests run against the composed variant. This optimizes pipeline execution time.
```yaml
# .github/workflows/build.yml
strategy:
matrix:
variant: [core, enterprise, healthcare]
steps:
- name: Build Variant
run: pnpm build:${{ matrix.variant }}
- name: Test Variant
run: pnpm test:${{ matrix.variant }}
- name: Asset Bundle
if: matrix.variant != 'core'
run: pnpm bundle-assets:${{ matrix.variant }}
```
Architecture Decisions and Rationale
- Monorepo vs. Polyrepo: Monorepo is mandatory for product lines. It enables atomic commits across core and variants, shared tooling, and immediate visibility into the impact of core changes. Polyrepo architectures fail to scale due to dependency synchronization overhead.
- Build-time vs. Runtime Variability: Prioritize build-time composition for structural differences (assets, routing, compliance modules). Reserve runtime variability for dynamic feature toggles that require immediate rollout without redeployment. Build-time composition reduces bundle size and eliminates runtime conditional checks.
- Asset Pipeline: Treat assets (CSS, images, localization files) as code. Version assets alongside variant definitions. Use a manifest-based approach to inject assets during the build process, ensuring that each variant bundle contains only necessary resources.
Pitfall Guide
-
Leaking Variant Logic into Core Services:
- Mistake: Adding
if (variant === 'healthcare') checks inside core billing or user management services.
- Consequence: Core services become coupled to specific variants, making it impossible to reuse the core for new variants without refactoring.
- Best Practice: Use dependency injection or strategy patterns. Core services should accept interfaces; variants provide implementations.
-
Over-Engineering the Core:
- Mistake: Building abstract hooks for every potential future variant requirement.
- Consequence: Unnecessary complexity in the core, increased maintenance burden, and YAGNI (You Aren't Gonna Need It) violations.
- Best Practice: Refactor the core only when a second variant requires the same abstraction. Use the Rule of Three.
-
Ignoring Database Migration Conflicts:
- Mistake: Allowing variants to run independent migration scripts that modify shared tables.
- Consequence: Schema conflicts, data corruption, and failed deployments when variants are updated asynchronously.
- Best Practice: Enforce a strict migration policy. Only core maintainers can modify shared tables. Variants can only create or modify extension tables.
-
Feature Flag Explosion:
- Mistake: Using feature flags for permanent variant differences (e.g.,
enable_sso_for_enterprise).
- Consequence: Flag debt accumulates, code becomes littered with conditional logic, and testing matrix explodes.
- Best Practice: Use feature flags only for temporary experiments or gradual rollouts. Permanent variant differences should be handled via composition.
-
Inconsistent Testing Strategies:
- Mistake: Running the full test suite for every variant on every commit, or skipping variant-specific tests.
- Consequence: CI pipelines become too slow, or variant regressions go undetected.
- Best Practice: Implement test impact analysis. Run core tests on core changes, and variant tests on variant changes. Run full matrix on release branches.
-
Hardcoding Variant Identifiers:
- Mistake: Using string literals like
'enterprise' or 'healthcare' throughout the codebase.
- Consequence: Refactoring becomes difficult, and typos cause runtime errors.
- Best Practice: Define variant identifiers as constants or enums in a shared package. Use type-safe configuration loaders.
-
Neglecting Security Boundaries:
- Mistake: Assuming all variants share the same security posture.
- Consequence: A vulnerability in a low-security variant (e.g., SMB tier) compromises the core, affecting high-security variants (e.g., Enterprise).
- Best Practice: Enforce security baselines in the core. Variants can add stricter controls but cannot relax core security requirements.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single Product, No Variants | Monolith with Feature Flags | Simplicity is paramount. Over-engineering for variability adds unnecessary complexity. | Low |
| 2-3 Variants, High Similarity | Core-Variant Monorepo | Shared logic dominates. Monorepo maximizes code reuse and reduces duplication. | Medium |
| 5+ Variants, Divergent Domains | Core-Variant Monorepo + Microservices | Core remains shared, but domain-specific services decouple heavy variant logic. | High |
| White-Label Requirements | Build-Time Composition | Branding and minor logic changes are best handled via asset injection and config overrides. | Low |
| Regulatory Compliance Variants | Core-Variant with Extension Tables | Data isolation and schema extensions ensure compliance without core modification. | Medium |
| Legacy Forked Architecture | Incremental Migration to Monorepo | Forks cannot be maintained at scale. Migrate shared code to core gradually. | Very High |
Configuration Template
Variant Definition Template (variant.config.ts)
// packages/core/src/variant/config.template.ts
import { VariantDefinition } from './types';
export const createVariant = (
id: string,
overrides: Partial<VariantDefinition>
): VariantDefinition => {
return {
id,
name: overrides.name || id,
features: overrides.features || {},
configOverrides: overrides.configOverrides || {},
assetManifest: overrides.assetManifest || {},
dependencies: overrides.dependencies || [],
};
};
// Usage in variant package
// packages/variants/healthcare/src/config.ts
import { createVariant } from '@codcompass/core/variant';
export default createVariant('healthcare', {
name: 'Healthcare Compliance',
features: {
hipaa: true,
patientPortal: true,
},
configOverrides: {
compliance: { hipaa: true, auditRetention: '7y' },
},
});
Turborepo Pipeline Configuration (turbo.json)
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"]
},
"lint": {
"inputs": ["src/**/*.ts", "src/**/*.tsx"]
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.ts", "src/**/*.tsx"]
},
"bundle:assets": {
"inputs": ["assets/**/*"],
"outputs": ["dist/assets/**/*"]
}
}
}
Quick Start Guide
-
Initialize Workspace:
Run npx create-turbo@latest saas-product-line and select TypeScript. Remove default apps and create /packages/core and /packages/variants.
-
Create Core Package:
Inside /packages/core, initialize a library package. Define the VariantDefinition interface and the composeConfig utility. Export a base configuration factory.
-
Create First Variant:
Inside /packages/variants, create a directory enterprise. Add a package.json with a dependency on @codcompass/core. Create config.ts using the template to define enterprise overrides.
-
Wire Build Pipeline:
Update turbo.json to include a build:variant task that accepts a variant name. Create a script in the root package.json: "build:variant": "turbo build --filter=./packages/variants/*".
-
Verify Composition:
Write a test in the variant package that imports composeConfig and asserts that enterprise features are enabled and core defaults are preserved. Run pnpm test to validate the pipeline.
-
Deploy Matrix:
Configure GitHub Actions to trigger on push. Use the matrix strategy to run pnpm build:variant for each variant. Archive build artifacts for each variant separately.
This strategy provides a robust engineering foundation for scaling SaaS product lines. By treating variability as a compositional concern and enforcing strict architectural boundaries, teams can deliver diverse offerings without sacrificing code quality, deployment velocity, or system reliability.