g, establish the boundary between Community and Enterprise features.
- Community: Core functionality, basic APIs, documentation, community plugins.
- Enterprise: SSO, RBAC, Audit Logs, High Availability, Priority Support, Advanced Analytics.
2. Architectural Separation
Use a monorepo structure with clear boundaries. This allows shared tooling while isolating enterprise code.
my-project/
βββ packages/
β βββ core/ # Open source, Apache 2.0
β βββ enterprise/ # Closed source, Commercial License
β βββ cli/ # CLI interface
β βββ plugins/ # Community plugin SDK
βββ .github/
βββ tsconfig.json
3. Implement Feature Gating
Use a runtime license checker to enforce feature boundaries. This prevents unauthorized use of enterprise features.
// packages/core/src/license/LicenseManager.ts
export type LicenseType = 'community' | 'enterprise';
export interface LicensePayload {
type: LicenseType;
features: Set<string>;
expiresAt?: Date;
}
export class LicenseManager {
private static instance: LicenseManager;
private payload: LicensePayload;
private constructor(payload: LicensePayload) {
this.payload = payload;
}
static initialize(payload: LicensePayload): void {
if (!LicenseManager.instance) {
LicenseManager.instance = new LicenseManager(payload);
}
}
static getInstance(): LicenseManager {
if (!LicenseManager.instance) {
// Default to community license if not initialized
LicenseManager.initialize({
type: 'community',
features: new Set(),
});
}
return LicenseManager.instance;
}
isFeatureAllowed(feature: string): boolean {
if (this.payload.type === 'enterprise') {
// Enterprise licenses can have specific feature restrictions
return this.payload.features.has(feature);
}
// Community users can only access features explicitly marked as community
// or not gated at all. Gating is handled at the call site.
return false;
}
getLicenseType(): LicenseType {
return this.payload.type;
}
}
4. Feature Gate Decorator
Apply decorators to enterprise features to automatically check permissions.
// packages/core/src/license/FeatureGate.ts
import { LicenseManager } from './LicenseManager';
export function EnterpriseFeature(featureName: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const license = LicenseManager.getInstance();
if (!license.isFeatureAllowed(featureName)) {
throw new Error(
`Feature '${featureName}' requires an Enterprise license. ` +
`Upgrade at https://example.com/upgrade.`
);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
5. Plugin Architecture
Enable community innovation without burdening the core team. This increases ecosystem value while keeping core maintainable.
// packages/core/src/plugins/PluginRegistry.ts
export interface Plugin {
name: string;
version: string;
install(context: PluginContext): void;
}
export interface PluginContext {
registerHook(hook: string, handler: Function): void;
getCoreService(serviceName: string): any;
}
export class PluginRegistry {
private plugins: Map<string, Plugin> = new Map();
register(plugin: Plugin): void {
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin '${plugin.name}' is already registered.`);
}
const context: PluginContext = {
registerHook: (hook, handler) => {
// Hook registration logic
console.log(`Hook '${hook}' registered by '${plugin.name}'`);
},
getCoreService: (serviceName) => {
// Service injection logic
return null;
},
};
plugin.install(context);
this.plugins.set(plugin.name, plugin);
}
}
6. Build Pipeline Configuration
Configure the build system to produce separate artifacts. The community package is published publicly; the enterprise package is distributed via private registry or binary.
// packages/core/package.json
{
"name": "@myorg/core",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build && node scripts/validate-community.js"
}
}
// packages/enterprise/package.json
{
"name": "@myorg/enterprise",
"version": "1.0.0",
"license": "Commercial",
"dependencies": {
"@myorg/core": "^1.0.0"
},
"scripts": {
"build": "tsc",
"publish": "npm publish --registry https://registry.myorg.com"
}
}
Architecture Decisions and Rationale
- Monorepo vs. Polyrepo: A monorepo is preferred for Open Core. It allows shared types, consistent tooling, and atomic changes across core and enterprise code. Polyrepos increase synchronization overhead and versioning complexity.
- Runtime vs. Build-time Gating: Runtime gating (as implemented) is more flexible. It allows the same binary to serve both community and enterprise users, reducing distribution complexity. Build-time gating creates separate binaries, which can lead to "feature drift" where community and enterprise versions diverge.
- Plugin SDK: Exposing a stable plugin API encourages third-party development. This extends the product's value proposition without increasing the core team's maintenance burden.
Pitfall Guide
1. The "Feature Gating" Trap
Mistake: Gating essential features that users expect in a base tool.
Explanation: If you gate basic functionality, users will fork the project or switch to a competitor. Gating must target enterprise requirements (compliance, scale, support), not core utility.
Best Practice: Conduct user research to identify the "minimum lovable product" for the community. Gate only features that drive enterprise procurement.
2. License Incompatibility
Mistake: Mixing copyleft licenses (e.g., GPL) with permissive licenses (e.g., Apache 2.0) in the same project.
Explanation: This creates legal hazards and prevents commercial use. If the core is GPL, the entire derivative work must be GPL, killing the Open Core model.
Best Practice: Use permissive licenses (Apache 2.0, MIT) for the core. Reserve copyleft or source-available licenses for specific components only if necessary.
3. CLA Friction
Mistake: Requiring Contributor License Agreements (CLAs) with complex terms.
Explanation: CLAs deter contributions. Developers abandon PRs when faced with legal paperwork.
Best Practice: Use the Developer Certificate of Origin (DCO) via sign-off. It provides sufficient legal protection with minimal friction. Automate DCO checks in CI.
4. The "Open Source" Definition Violation
Mistake: Marketing source-available software as "Open Source" when it violates the OSI definition.
Explanation: This damages trust. The community will reject projects that misuse terminology.
Best Practice: Be precise. Use "Source Available" for non-OSI licenses. If you use SSPL or BSL, acknowledge the restrictions transparently.
5. Security Debt
Mistake: Ignoring security updates because the software is free.
Explanation: Enterprises will not adopt OSS with known CVEs. Security is a prerequisite for commercial viability.
Best Practice: Implement automated dependency scanning (e.g., Dependabot, Snyk). Maintain a security policy and a responsible disclosure process.
6. Monorepo Bloat
Mistake: Mixing private infrastructure code with public OSS code.
Explanation: This risks leaking secrets or internal logic. It also confuses contributors who see irrelevant code.
Best Practice: Use workspace boundaries. Ensure CI pipelines strip sensitive data before publishing community packages.
Mistake: Treating contributors as free labor without recognition or governance.
Explanation: Contributors leave if they feel undervalued. A healthy community requires transparent decision-making.
Best Practice: Establish a governance model. Recognize top contributors. Allow community input on the roadmap, even if final decisions rest with the commercial entity.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Infrastructure Tool | Open Core + SSPL/BSL | Prevents cloud providers from reselling without paying. | High dev effort for gating; legal complexity. |
| Developer Library | Apache 2.0 + Sponsorships | Maximize adoption; libraries are hard to gate. | Low revenue; reliance on grants/sponsorships. |
| Enterprise Platform | Dual Licensing | Enterprises need commercial indemnity and support. | High sales effort; compliance overhead. |
| Niche Utility | Managed SaaS | Low maintenance; direct value capture. | Infrastructure costs; limited scalability. |
| Protocol/Standard | Apache 2.0 + Certification | Establish standard; monetize via certification. | Ecosystem building required; slow revenue. |
Configuration Template
GitHub Actions: Dual Build & Publish
# .github/workflows/publish.yml
name: Publish Packages
on:
push:
tags:
- 'v*'
jobs:
publish-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build --workspace=@myorg/core
- run: npm publish --workspace=@myorg/core
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-enterprise:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.myorg.com'
- run: npm ci
- run: npm run build --workspace=@myorg/enterprise
- run: npm publish --workspace=@myorg/enterprise
env:
NODE_AUTH_TOKEN: ${{ secrets.PRIVATE_NPM_TOKEN }}
License Validation Script
// scripts/validate-community.js
const fs = require('fs');
const path = require('path');
const enterpriseKeywords = ['enterprise', 'commercial', 'sso', 'rbac'];
const corePath = path.join(__dirname, '..', 'packages', 'core', 'src');
function checkForEnterpriseRefs(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
checkForEnterpriseRefs(filePath);
} else if (file.endsWith('.ts') || file.endsWith('.js')) {
const content = fs.readFileSync(filePath, 'utf8');
for (const keyword of enterpriseKeywords) {
if (content.includes(keyword)) {
console.warn(`Warning: Potential enterprise reference '${keyword}' in ${filePath}`);
}
}
}
}
}
checkForEnterpriseRefs(corePath);
console.log('Validation complete.');
Quick Start Guide
-
Initialize Repository:
mkdir my-oss-project && cd my-oss-project
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
-
Set Up Structure:
mkdir -p packages/core/src packages/enterprise/src
# Copy package.json templates and source files from Core Solution
-
Configure Build:
# Update root package.json with workspaces
# Add "workspaces": ["packages/*"]
npm install
npm run build
-
Add License & Governance:
echo "Apache License 2.0" > LICENSE
echo "See CONTRIBUTING.md" > CONTRIBUTING.md
# Create CONTRIBUTING.md with DCO instructions
-
Publish Core:
npm publish --workspace=@myorg/core
# Verify community package is accessible and enterprise features are gated
By engineering the business model into the architecture from day one, you ensure that the project remains sustainable, legally compliant, and attractive to both the community and enterprise customers. Open source is not just code; it is a system of incentives, and your architecture must align with those incentives.