Configuration Sync Service
The Configuration Sync Service allows you to define all products, features, plans, and billing cycles in a single JSON configuration file or programmatically, then sync them to the database. This is ideal for version-controlled configuration management and infrastructure-as-code workflows.
Overview
The sync service: - Creates entities that don't exist in the database - Updates existing entities with new values - Archives/Unarchives entities based on the archived flag - Syncs associations (product-feature, plan-feature values) - Ignores entities not in the config (leaves them unchanged) - Validates all references and data types before syncing
Quick Start
Option 1: Sync from JSON File
import { Subscrio } from '@subscrio/core';
const subscrio = new Subscrio({
database: { connectionString: process.env.DATABASE_URL! }
});
// Sync from a JSON file
const report = await subscrio.configSync.syncFromFile('./config.json');
console.log(`Created: ${report.created.features} features, ${report.created.products} products`);
console.log(`Updated: ${report.updated.features} features, ${report.updated.products} products`);
Option 2: Sync from Programmatic Config
import { Subscrio, ConfigSyncDto } from '@subscrio/core';
const subscrio = new Subscrio({
database: { connectionString: process.env.DATABASE_URL! }
});
// Build config programmatically
const config: ConfigSyncDto = {
version: '1.0',
features: [
{
key: 'max-projects',
displayName: 'Maximum Projects',
valueType: 'numeric',
defaultValue: '10'
}
],
products: [
{
key: 'project-management',
displayName: 'Project Management',
features: ['max-projects'],
plans: [
{
key: 'basic',
displayName: 'Basic Plan',
featureValues: {
'max-projects': '5'
},
billingCycles: [
{
key: 'monthly',
displayName: 'Monthly',
durationValue: 1,
durationUnit: 'months'
}
]
}
]
}
]
};
const report = await subscrio.configSync.syncFromJson(config);
JSON Schema
Root Configuration
CRITICAL: The features array MUST appear before the products array in JSON files.
Feature Configuration
interface FeatureConfig {
key: string; // Required, globally unique, immutable
displayName: string; // Required
description?: string; // Optional
valueType: 'toggle' | 'numeric' | 'text'; // Required
defaultValue: string; // Required, validated against valueType
groupName?: string; // Optional
validator?: Record<string, unknown>; // Optional
metadata?: Record<string, unknown>; // Optional
archived?: boolean; // Optional, defaults to false
}
Example:
{
"key": "max-projects",
"displayName": "Maximum Projects",
"description": "Maximum number of projects allowed",
"valueType": "numeric",
"defaultValue": "1",
"groupName": "Limits",
"archived": false
}
Product Configuration
interface ProductConfig {
key: string; // Required, globally unique, immutable
displayName: string; // Required
description?: string; // Optional
metadata?: Record<string, unknown>; // Optional
archived?: boolean; // Optional, defaults to false
features?: string[]; // Optional, array of feature keys
plans?: PlanConfig[]; // Optional, nested plans
}
Example:
{
"key": "project-management",
"displayName": "Project Management",
"description": "Complete project management solution",
"archived": false,
"features": ["max-projects", "team-size", "gantt-charts"],
"plans": [...]
}
Plan Configuration
interface PlanConfig {
key: string; // Required, unique within product, immutable
displayName: string; // Required
description?: string; // Optional
onExpireTransitionToBillingCycleKey?: string; // Optional, must reference billing cycle in any plan within same product
metadata?: Record<string, unknown>; // Optional
archived?: boolean; // Optional, defaults to false
featureValues?: Record<string, string>; // Optional, feature key -> value mapping
billingCycles?: BillingCycleConfig[]; // Optional, nested billing cycles
}
Example:
{
"key": "basic",
"displayName": "Basic Plan",
"description": "For small teams",
"archived": false,
"featureValues": {
"max-projects": "5",
"gantt-charts": "false"
},
"billingCycles": [...]
}
Billing Cycle Configuration
interface BillingCycleConfig {
key: string; // Required, unique within plan, immutable
displayName: string; // Required
description?: string; // Optional
durationValue?: number; // Required if durationUnit !== 'forever'
durationUnit: 'days' | 'weeks' | 'months' | 'years' | 'forever'; // Required
externalProductId?: string; // Optional, e.g., Stripe price ID
archived?: boolean; // Optional, defaults to false
}
Example:
{
"key": "monthly",
"displayName": "Monthly",
"description": "Monthly billing cycle",
"durationValue": 1,
"durationUnit": "months",
"externalProductId": "price_stripe_monthly",
"archived": false
}
Complete Example
{
"version": "1.0",
"features": [
{
"key": "max-projects",
"displayName": "Maximum Projects",
"description": "Maximum number of projects allowed",
"valueType": "numeric",
"defaultValue": "1",
"groupName": "Limits"
},
{
"key": "gantt-charts",
"displayName": "Gantt Charts",
"description": "Enable Gantt chart visualization",
"valueType": "toggle",
"defaultValue": "false",
"groupName": "Features"
}
],
"products": [
{
"key": "project-management",
"displayName": "Project Management",
"description": "Complete project management solution",
"archived": false,
"features": ["max-projects", "gantt-charts"],
"plans": [
{
"key": "basic",
"displayName": "Basic Plan",
"description": "For small teams",
"archived": false,
"featureValues": {
"max-projects": "5",
"gantt-charts": "false"
},
"billingCycles": [
{
"key": "monthly",
"displayName": "Monthly",
"durationValue": 1,
"durationUnit": "months",
"archived": false
},
{
"key": "yearly",
"displayName": "Yearly",
"durationValue": 1,
"durationUnit": "years",
"archived": false
}
]
},
{
"key": "pro",
"displayName": "Pro Plan",
"description": "For growing teams",
"archived": false,
"featureValues": {
"max-projects": "50",
"gantt-charts": "true"
},
"billingCycles": [
{
"key": "monthly",
"displayName": "Monthly",
"durationValue": 1,
"durationUnit": "months",
"externalProductId": "price_stripe_monthly",
"archived": false
}
]
}
]
}
]
}
Sync Behavior
Create Operations
Entities in the config that don't exist in the database are created: - Features are created first (independent entities) - Products are created next - Plans are created for each product - Billing cycles are created for each plan
Update Operations
Entities that exist in both config and database are updated: - Only fields specified in the config are updated - Keys are immutable and cannot be changed - Archive status is handled separately (see below)
Archive Operations
The archived boolean property controls entity status:
archived: true- Sets entity status toarchivedarchived: falseor omitted - Sets entity status toactive
Archive operations use the entity's archive() and unarchive() methods, ensuring business rules are followed.
Association Sync
Product-Feature Associations: - Features listed in product.features are associated - Features not listed are dissociated - Only features explicitly in the config are synced
Plan Feature Values: - Feature values in plan.featureValues are set - Feature values not in config are removed - Values are validated against feature valueType
Ignore Behavior
Entities in the database but not in the config are: - Completely ignored - no changes made - Counted in the sync report's ignored section - Left unchanged - status, associations, and values remain as-is
This allows partial syncs where you only update specific entities.
Sync Report
The sync service returns a detailed report:
interface ConfigSyncReport {
created: {
features: number;
products: number;
plans: number;
billingCycles: number;
};
updated: {
features: number;
products: number;
plans: number;
billingCycles: number;
};
archived: {
features: number;
products: number;
plans: number;
billingCycles: number;
};
unarchived: {
features: number;
products: number;
plans: number;
billingCycles: number;
};
ignored: {
features: number;
products: number;
plans: number;
billingCycles: number;
};
errors: Array<{
entityType: 'feature' | 'product' | 'plan' | 'billingCycle';
key: string;
message: string;
}>;
warnings: Array<{
entityType: 'feature' | 'product' | 'plan' | 'billingCycle';
key: string;
message: string;
}>;
}
Example Usage:
const report = await subscrio.configSync.syncFromJson(config);
if (report.errors.length > 0) {
console.error('Sync errors:');
report.errors.forEach(error => {
console.error(` ${error.entityType} ${error.key}: ${error.message}`);
});
}
console.log(`Sync complete: ${report.created.features} features created`);
Validation
The sync service performs comprehensive validation:
Schema Validation
- All required fields are present
- Field types match expected types
- String lengths within limits
- Enum values are valid
JSON Property Order
- CRITICAL:
featuresmust appear beforeproductsin JSON files - Validation throws
ValidationErrorif order is incorrect
Duplicate Key Validation
- Feature keys must be globally unique
- Product keys must be globally unique
- Plan keys must be unique within product
- Billing cycle keys must be unique within plan
Reference Validation
- All feature keys referenced in products must exist in features array
- All feature keys in
plan.featureValuesmust be associated with the product onExpireTransitionToBillingCycleKeymust reference a valid billing cycle in any plan within the same product
Feature Value Validation
- Toggle features: values must be
"true"or"false" - Numeric features: values must be valid numbers
- Text features: any string value is accepted
Error Handling
Validation Errors
Validation errors are thrown before any sync operations:
try {
await subscrio.configSync.syncFromJson(config);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation failed:', error.message);
console.error('Details:', error.errors);
}
}
Sync Errors
Errors during sync operations are collected in the report:
const report = await subscrio.configSync.syncFromJson(config);
if (report.errors.length > 0) {
// Some operations failed, but others may have succeeded
// Operations are idempotent, so you can re-run sync
console.error('Some operations failed:', report.errors);
}
Partial Completion
Since operations are idempotent, if an error occurs: 1. Completed operations remain in the database 2. Failed operations are reported in report.errors 3. You can re-run sync to complete remaining operations
Best Practices
1. Version Control Your Config
Store configuration files in version control:
2. Use Programmatic Config for Dynamic Generation
function generateConfig(environment: string): ConfigSyncDto {
const baseFeatures = [...];
const environmentFeatures = getEnvironmentFeatures(environment);
return {
version: '1.0',
features: [...baseFeatures, ...environmentFeatures],
products: [...]
};
}
await subscrio.configSync.syncFromJson(generateConfig('production'));
3. Validate Before Production
Always validate your config before syncing to production:
import { ConfigSyncDtoSchema } from '@subscrio/core';
try {
const config = ConfigSyncDtoSchema.parse(jsonData);
// Config is valid, safe to sync
await subscrio.configSync.syncFromJson(config);
} catch (error) {
console.error('Config validation failed:', error);
process.exit(1);
}
4. Handle Errors Gracefully
const report = await subscrio.configSync.syncFromJson(config);
if (report.errors.length > 0) {
// Log errors but don't fail the entire process
logger.error('Sync completed with errors', { errors: report.errors });
// Optionally re-run for failed operations
if (shouldRetry(report.errors)) {
await subscrio.configSync.syncFromJson(config);
}
}
5. Use Partial Syncs
Only include entities you want to update:
// Only update features, leave products unchanged
const partialConfig: ConfigSyncDto = {
version: '1.0',
features: [
{ key: 'new-feature', displayName: 'New Feature', valueType: 'toggle', defaultValue: 'false' }
],
products: [] // Empty - products won't be touched
};
await subscrio.configSync.syncFromJson(partialConfig);
6. Archive Instead of Delete
Use archived: true to mark entities as archived rather than deleting them:
{
"key": "old-feature",
"displayName": "Old Feature",
"valueType": "toggle",
"defaultValue": "false",
"archived": true
}
Common Patterns
Feature Flags
const config: ConfigSyncDto = {
version: '1.0',
features: [
{
key: 'beta-feature',
displayName: 'Beta Feature',
valueType: 'toggle',
defaultValue: 'false'
}
],
products: [
{
key: 'main-product',
displayName: 'Main Product',
features: ['beta-feature'],
plans: [
{
key: 'premium',
displayName: 'Premium',
featureValues: {
'beta-feature': 'true' // Enable for premium plan
}
}
]
}
]
};
Tiered Plans
const config: ConfigSyncDto = {
version: '1.0',
features: [
{ key: 'storage-gb', displayName: 'Storage (GB)', valueType: 'numeric', defaultValue: '1' }
],
products: [
{
key: 'storage-product',
displayName: 'Storage Product',
features: ['storage-gb'],
plans: [
{
key: 'basic',
displayName: 'Basic',
featureValues: { 'storage-gb': '10' }
},
{
key: 'pro',
displayName: 'Pro',
featureValues: { 'storage-gb': '100' }
},
{
key: 'enterprise',
displayName: 'Enterprise',
featureValues: { 'storage-gb': '1000' }
}
]
}
]
};
Limitations
- No Delete Operations: Entities can only be archived, not deleted
- Immutable Keys: Keys cannot be changed after creation
- Sequential Operations: Operations run sequentially (not in a transaction)
- Partial Completion: If an error occurs, some operations may have completed
Troubleshooting
"features must appear before products" Error
Problem: JSON property order is incorrect.
Solution: Ensure features array comes before products array in your JSON file.
"Feature key 'X' referenced in product does not exist" Error
Problem: Product references a feature that's not in the features array.
Solution: Add the feature to the features array, or remove it from product.features.
"Invalid feature value for numeric type" Error
Problem: Plan feature value doesn't match feature's valueType.
Solution: Ensure numeric features have numeric values, toggle features have "true"/"false".
Sync Report Shows Errors
Problem: Some operations failed during sync.
Solution: 1. Check report.errors for details 2. Fix the issues in your config 3. Re-run sync (operations are idempotent)
API Reference
syncFromFile(filePath: string): Promise<ConfigSyncReport>
Loads configuration from a JSON file and syncs.
Parameters: - filePath: Path to JSON configuration file
Returns: Promise<ConfigSyncReport>
Throws: - ValidationError if file is invalid or JSON property order is incorrect - Error if file cannot be read
syncFromJson(config: ConfigSyncDto): Promise<ConfigSyncReport>
Syncs configuration from a ConfigSyncDto object.
Parameters: - config: Configuration object (can be built programmatically)
Returns: Promise<ConfigSyncReport>
Throws: - ValidationError if config is invalid