Configuration Sync Service Reference
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.
Accessing the Service
Method Catalog
| Method | Description | Returns |
|---|---|---|
syncFromFile | Loads configuration from a JSON file and syncs to the database | Promise<ConfigSyncReport> |
syncFromJson | Syncs configuration from a ConfigSyncDto object | Promise<ConfigSyncReport> |
| Method | Description | Returns |
|---|---|---|
SyncFromFileAsync | Loads configuration from a JSON file and syncs to the database | Task<ConfigSyncReport> |
SyncFromJsonAsync | Syncs configuration from a ConfigSyncDto object | Task<ConfigSyncReport> |
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 'core.typescript';
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`);
using Subscrio.Core;
var subscrio = new Subscrio(config);
var report = await subscrio.ConfigSync.SyncFromFileAsync("./config.json");
Console.WriteLine($"Created: {report.Created.Features} features, {report.Created.Products} products");
Console.WriteLine($"Updated: {report.Updated.Features} features, {report.Updated.Products} products");
Option 2: Sync from Programmatic Config
import { Subscrio, ConfigSyncDto } from 'core.typescript';
const subscrio = new Subscrio({
database: { connectionString: process.env.DATABASE_URL! }
});
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);
using Subscrio.Core;
using Subscrio.Core.Application.DTOs;
var subscrio = new Subscrio(config);
var config = new ConfigSyncDto(
Version: "1.0",
Features: new List<FeatureConfig>
{
new FeatureConfig("max-projects", "Maximum Projects", ValueType: "numeric", DefaultValue: "10")
},
Products: new List<ProductConfig>
{
new ProductConfig(
"project-management",
"Project Management",
Features: new List<string> { "max-projects" },
Plans: new List<PlanConfig>
{
new PlanConfig(
"basic",
"Basic Plan",
FeatureValues: new Dictionary<string, string> { ["max-projects"] = "5" },
BillingCycles: new List<BillingCycleConfig>
{
new BillingCycleConfig("monthly", "Monthly", DurationValue: 1, DurationUnit: "months")
}
)
}
)
}
);
var report = await subscrio.ConfigSync.SyncFromJsonAsync(config);
Option 3: Initial config at construction
You can pass the same config sync input to the Subscrio constructor via initialConfig (TypeScript) or InitialConfig (.NET). After construction, call runInitialConfigSync() / RunInitialConfigSyncAsync() to apply it (e.g. after installing or verifying the schema).
import { Subscrio } from 'core.typescript';
const subscrio = new Subscrio({
database: { connectionString: process.env.DATABASE_URL! },
initialConfig: { type: 'file', filePath: './config.json' }
// or: initialConfig: { type: 'json', config: myConfigSyncDto }
});
await subscrio.installSchema();
const report = await subscrio.runInitialConfigSync(); // applies initial config, or null if none
using Subscrio.Core;
using Subscrio.Core.Config;
var subscrio = new Subscrio(new SubscrioConfig
{
Database = new DatabaseConfig { ConnectionString = "..." },
InitialConfig = new InitialConfigOptions { FilePath = "./config.json" }
// or: InitialConfig = new InitialConfigOptions { Config = myConfigSyncDto }
});
await subscrio.InstallSchemaAsync();
var report = await subscrio.RunInitialConfigSyncAsync(); // applies initial config, or null if none
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
}
public record PlanConfig(
string Key,
string DisplayName,
string? Description = null,
string? OnExpireTransitionToBillingCycleKey = null,
Dictionary<string, string>? FeatureValues = null,
List<BillingCycleConfig>? BillingCycles = null,
Dictionary<string, object?>? Metadata = null,
bool? Archived = null
);
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
The same JSON config file works for both TypeScript and .NET:
{
"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;
}>;
}
public record ConfigSyncCounts(int Features, int Products, int Plans, int BillingCycles);
public record ConfigSyncError(string EntityType, string Key, string Message);
public record ConfigSyncWarning(string EntityType, string Key, string Message);
public record ConfigSyncReport(
ConfigSyncCounts Created,
ConfigSyncCounts Updated,
ConfigSyncCounts Archived,
ConfigSyncCounts Unarchived,
ConfigSyncCounts Ignored,
List<ConfigSyncError> Errors,
List<ConfigSyncWarning> Warnings
);
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`);
var report = await subscrio.ConfigSync.SyncFromJsonAsync(config);
if (report.Errors.Count > 0)
{
Console.Error.WriteLine("Sync errors:");
foreach (var error in report.Errors)
{
Console.Error.WriteLine($" {error.EntityType} {error.Key}: {error.Message}");
}
}
Console.WriteLine($"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:
Sync Errors
Errors during sync operations are collected in the report:
var report = await subscrio.ConfigSync.SyncFromJsonAsync(config);
if (report.Errors.Count > 0)
{
// Some operations failed, but others may have succeeded
// Operations are idempotent, so you can re-run sync
Console.Error.WriteLine("Some operations failed:");
foreach (var err in report.Errors)
Console.Error.WriteLine($" {err.EntityType} {err.Key}: {err.Message}");
}
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'));
ConfigSyncDto GenerateConfig(string environment)
{
var baseFeatures = new List<FeatureConfig> { /* ... */ };
var environmentFeatures = GetEnvironmentFeatures(environment);
return new ConfigSyncDto(
Version: "1.0",
Features: baseFeatures.Concat(environmentFeatures).ToList(),
Products: new List<ProductConfig> { /* ... */ }
);
}
await subscrio.ConfigSync.SyncFromJsonAsync(GenerateConfig("production"));
3. Validate Before Production
Always validate your config before syncing to production:
try
{
var config = JsonSerializer.Deserialize<ConfigSyncDto>(jsonData,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (config == null) throw new InvalidOperationException("Invalid config");
await subscrio.ConfigSync.SyncFromJsonAsync(config);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Config validation failed: {ex.Message}");
Environment.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);
// Only update features, leave products unchanged
var partialConfig = new ConfigSyncDto(
Version: "1.0",
Features: new List<FeatureConfig>
{
new FeatureConfig("new-feature", "New Feature", ValueType: "toggle", DefaultValue: "false")
},
Products: new List<ProductConfig>() // Empty - products won't be touched
);
await subscrio.ConfigSync.SyncFromJsonAsync(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
}
}
]
}
]
};
var config = new ConfigSyncDto(
Version: "1.0",
Features: new List<FeatureConfig>
{
new FeatureConfig("beta-feature", "Beta Feature", ValueType: "toggle", DefaultValue: "false")
},
Products: new List<ProductConfig>
{
new ProductConfig(
"main-product",
"Main Product",
Features: new List<string> { "beta-feature" },
Plans: new List<PlanConfig>
{
new PlanConfig(
"premium",
"Premium",
FeatureValues: new Dictionary<string, string> { ["beta-feature"] = "true" }
)
}
)
}
);
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' }
}
]
}
]
};
var config = new ConfigSyncDto(
Version: "1.0",
Features: new List<FeatureConfig>
{
new FeatureConfig("storage-gb", "Storage (GB)", ValueType: "numeric", DefaultValue: "1")
},
Products: new List<ProductConfig>
{
new ProductConfig(
"storage-product",
"Storage Product",
Features: new List<string> { "storage-gb" },
Plans: new List<PlanConfig>
{
new PlanConfig("basic", "Basic", FeatureValues: new Dictionary<string, string> { ["storage-gb"] = "10" }),
new PlanConfig("pro", "Pro", FeatureValues: new Dictionary<string, string> { ["storage-gb"] = "100" }),
new PlanConfig("enterprise", "Enterprise", FeatureValues: new Dictionary<string, string> { ["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)
Method Reference
syncFromFile
Description
Loads configuration from a JSON file, validates it, and syncs products, features, plans, and billing cycles to the database. The features array must appear before the products array in the JSON file.
Signature
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
filePath | string | Yes | Path to the JSON configuration file. |
Returns
Promise<ConfigSyncReport> – sync report with counts for created, updated, archived, unarchived, and ignored entities.
Return Properties
| Field | Type | Description |
|---|---|---|
created | { features, products, plans, billingCycles } | Counts of entities created. |
updated | { features, products, plans, billingCycles } | Counts of entities updated. |
archived | { features, products, plans, billingCycles } | Counts of entities archived. |
unarchived | { features, products, plans, billingCycles } | Counts of entities unarchived. |
ignored | { features, products, plans, billingCycles } | Counts of entities not in config (left unchanged). |
errors | Array<{ message, entityType?, key? }> | Errors encountered during sync. |
warnings | string[] | Non-fatal warnings. |
Example
Signature
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
filePath | string | Yes | Path to the JSON configuration file. |
Returns
Task<ConfigSyncReport> – sync report with counts for created, updated, archived, unarchived, and ignored entities.
Return Properties
| Property | Type | Description |
|---|---|---|
Created | ConfigSyncCounts | Counts of entities created. |
Updated | ConfigSyncCounts | Counts of entities updated. |
Archived | ConfigSyncCounts | Counts of entities archived. |
Unarchived | ConfigSyncCounts | Counts of entities unarchived. |
Ignored | ConfigSyncCounts | Counts of entities not in config (left unchanged). |
Errors | List<ConfigSyncError> | Errors encountered during sync. |
Warnings | List<string> | Non-fatal warnings. |
Example
Expected Results
- Reads and parses the JSON file.
- Validates JSON property order (
featuresbeforeproducts). - Validates config schema and all references.
- Creates, updates, archives, and unarchives entities as needed.
- Returns a detailed report.
Potential Errors
| Error | When |
|---|---|
ValidationError | File invalid, JSON property order incorrect, or config schema validation fails. |
Error | File cannot be read (e.g., file not found). |
syncFromJson
Description
Syncs configuration from a ConfigSyncDto object. Use when building config programmatically rather than loading from a file.
Signature
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
config | ConfigSyncDto | Yes | Configuration object (features, products with nested plans and billing cycles). |
Returns
Promise<ConfigSyncReport> – same structure as syncFromFile.
Return Properties
Same as syncFromFile (see ConfigSyncReport).
Example
const config: ConfigSyncDto = {
version: '1.0',
features: [{ key: 'max-projects', displayName: 'Max Projects', valueType: 'numeric', defaultValue: '10' }],
products: [{ key: 'my-product', displayName: 'My Product', features: ['max-projects'], plans: [] }]
};
const report = await subscrio.configSync.syncFromJson(config);
Signature
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
config | ConfigSyncDto | Yes | Configuration object (features, products with nested plans and billing cycles). |
Returns
Task<ConfigSyncReport> – same structure as SyncFromFileAsync.
Return Properties
Same as SyncFromFileAsync (see ConfigSyncReport).
Example
var config = new ConfigSyncDto(
Version: "1.0",
Features: new List<FeatureConfig> { new("max-projects", "Max Projects", ValueType: "numeric", DefaultValue: "10") },
Products: new List<ProductConfig> { new("my-product", "My Product", Features: new List<string> { "max-projects" }, Plans: new List<PlanConfig>()) }
);
var report = await subscrio.ConfigSync.SyncFromJsonAsync(config);
Expected Results
- Validates config schema and all references.
- Creates, updates, archives, and unarchives entities as needed.
- Returns a detailed report.
Potential Errors
| Error | When |
|---|---|
ValidationError | Config schema invalid or references break (e.g., plan references non-existent product). |