Subscription and entitlement engine for .NET. Products, plans, features, customers, subscriptions, and feature resolution with PostgreSQL or SQL Server.
$ dotnet add package Subscrio.CoreA .NET implementation of the Subscrio subscription management library for managing products, plans, features, customers, subscriptions, and billing cycles.
The entitlement engine that translates subscriptions into feature access.
See the main README for an overview of Subscrio's concepts, architecture, and features.
Add the NuGet package to your project:
<PackageReference Include="Subscrio.Core" Version="1.0.0" />
Or using the .NET CLI:
dotnet add package Subscrio.Core
Prerequisites:
The library is multi-targeted: one NuGet package includes builds for all supported frameworks; the correct one is chosen by your project's target framework.
You need a database before using Subscrio. The library does not create the database itself; it only installs and updates schema inside an existing database.
PostgreSQL
subscrio) using psql, pgAdmin, or your host’s tooling.psql:
psql -U postgres -c "CREATE DATABASE subscrio;"
SQL Server
Subscrio) in SQL Server Management Studio or with T-SQL:
CREATE DATABASE Subscrio;
Set the connection string in one of these ways:
Environment variable (recommended for local and production)
Set DATABASE_URL:
Host=localhost;Port=5432;Database=subscrio;Username=postgres;Password=yourpasswordServer=localhost;Database=Subscrio;User Id=sa;Password=yourpassword;TrustServerCertificate=trueConfigLoader.Load()
ConfigLoader.Load() reads DATABASE_URL (and optionally DATABASE_TYPE, STRIPE_SECRET_KEY) from the process environment. Use this when you configure the app via env vars or launchSettings.
Explicit config in code
Build a SubscrioConfig and set Database.ConnectionString and Database.DatabaseType (e.g. DatabaseType.PostgreSQL or DatabaseType.SqlServer). Use this for custom config sources (e.g. key vault, appsettings).
ASP.NET Core appsettings
You can load connection strings from appsettings.json or other configuration and pass them into the constructor or into the object you use to build SubscrioConfig.
After the database exists and the connection string is set, use Database setup and migrations below to install or update the schema.
From the repo root or from core.dotnet:
cd core.dotnet
dotnet build Subscrio.Core.sln
dotnet test Subscrio.Core.sln
Tests expect a running PostgreSQL instance and use the connection string from the TEST_DATABASE_URL environment variable (or a default local connection string). See tests/README.md for test setup.
using Subscrio.Core;
using Subscrio.Core.Config;
// Load configuration
var config = ConfigLoader.Load();
// Initialize the library
var subscrio = new Subscrio(config);
// Install database schema (first time only)
await subscrio.InstallSchemaAsync("your-admin-passphrase");
// Create a product
var product = await subscrio.Products.CreateProductAsync(new CreateProductDto(
Key: "my-saas",
DisplayName: "My SaaS Product"
));
// Create a feature
var feature = await subscrio.Features.CreateFeatureAsync(new CreateFeatureDto(
Key: "max-users",
DisplayName: "Maximum Users",
ValueType: FeatureValueType.Numeric,
DefaultValue: "10"
));
// Associate feature with product (using keys, not IDs)
await subscrio.Products.AssociateFeatureAsync(product.Key, feature.Key);
// Create a plan (using productKey, not productId)
var plan = await subscrio.Plans.CreatePlanAsync(new CreatePlanDto(
ProductKey: product.Key,
Key: "pro-plan",
DisplayName: "Pro Plan"
));
// Set feature value on plan (using keys, not IDs)
await subscrio.Plans.SetFeatureValueAsync(plan.Key, feature.Key, "100");
// Create a billing cycle for the plan (required for subscriptions)
var billingCycle = await subscrio.BillingCycles.CreateBillingCycleAsync(new CreateBillingCycleDto(
PlanKey: plan.Key,
Key: "monthly",
DisplayName: "Monthly",
DurationValue: 1,
DurationUnit: "months"
));
// Create a customer (using key, not externalId)
var customer = await subscrio.Customers.CreateCustomerAsync(new CreateCustomerDto(
Key: "customer-123",
DisplayName: "Acme Corp"
));
// Create a subscription (using keys and billingCycleKey, not IDs)
var subscription = await subscrio.Subscriptions.CreateSubscriptionAsync(new CreateSubscriptionDto(
Key: "sub-001",
CustomerKey: customer.Key,
BillingCycleKey: billingCycle.Key
));
// Check feature access (requires customerKey, productKey, and featureKey)
var maxUsers = await subscrio.FeatureChecker.GetValueForCustomerAsync(
customer.Key,
product.Key,
"max-users"
);
Console.WriteLine($"Customer can have {maxUsers} users"); // "100"
Subscrio supports dependency injection and can be registered in your DI container. This is the recommended approach for web applications and provides better lifetime management.
using Subscrio.Core;
using Subscrio.Core.Config;
var builder = WebApplication.CreateBuilder(args);
// Load configuration
var config = ConfigLoader.Load();
// Register Subscrio with Scoped lifetime (recommended for web apps)
builder.Services.AddSubscrio(config, ServiceLifetime.Scoped);
var app = builder.Build();
// Use in controllers
app.MapGet("/products", async (Subscrio subscrio) =>
{
var products = await subscrio.Products.ListProductsAsync();
return Results.Ok(products);
});
app.Run();
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Subscrio.Core;
using Subscrio.Core.Config;
var config = ConfigLoader.Load();
var services = new ServiceCollection();
services.AddSubscrio(config, ServiceLifetime.Scoped);
var serviceProvider = services.BuildServiceProvider();
// Use scoped service
using var scope = serviceProvider.CreateScope();
var subscrio = scope.ServiceProvider.GetRequiredService<Subscrio>();
var products = await subscrio.Products.ListProductsAsync();
using Microsoft.AspNetCore.Mvc;
using Subscrio.Core;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly Subscrio _subscrio;
public ProductsController(Subscrio subscrio)
{
_subscrio = subscrio; // Injected by DI
}
[HttpGet]
public async Task<ActionResult<List<ProductDto>>> GetProducts()
{
var products = await _subscrio.Products.ListProductsAsync();
return Ok(products);
}
[HttpPost]
public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductDto dto)
{
var product = await _subscrio.Products.CreateProductAsync(dto);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}
The AddSubscrio() method accepts a ServiceLifetime parameter:
services.AddSubscrio(config, ServiceLifetime.Scoped);
Subscrio instance per HTTP requestSubscrio instanceDbContextservices.AddSubscrio(config, ServiceLifetime.Transient);
Subscrio instance every time it's requestedservices.AddSubscrio(config, ServiceLifetime.Singleton);
Subscrio instance shared across the entire applicationSubscrio creates its own DbContext internally when instantiated. This means:
Subscrio → new DbContext → clean change trackerSubscrio → new DbContext → clean change trackerSubscrio → single DbContext → potential tracking conflictsBest Practice: Use ServiceLifetime.Scoped for web applications to ensure proper isolation and resource management.
subscrio.Products - Product managementsubscrio.Features - Feature flag managementsubscrio.Plans - Subscription plan managementsubscrio.BillingCycles - Billing cycle managementsubscrio.Customers - Customer managementsubscrio.Subscriptions - Subscription lifecyclesubscrio.FeatureChecker - Feature access checkingsubscrio.Stripe - Stripe integrationInstallSchemaAsync(adminPassphrase) - Install database schemaVerifySchemaAsync() - Check if schema is installed (returns version string or null)MigrateAsync() - Run database migrationsDispose() - Clean up resources (implements IDisposable)var config = new SubscrioConfig
{
Database = new DatabaseConfig
{
ConnectionString = "Host=localhost;Port=5432;Database=subscrio;Username=postgres;Password=password",
DatabaseType = DatabaseType.PostgreSQL, // or DatabaseType.SqlServer
Ssl = false,
PoolSize = 10
},
Stripe = new StripeConfig
{
SecretKey = "sk_test_..." // Optional
}
};
var config = ConfigLoader.Load();
This reads from:
DATABASE_URL - Database connection stringDATABASE_TYPE - "PostgreSQL" or "SqlServer"STRIPE_SECRET_KEY - Stripe secret key (optional)var config = new SubscrioConfig
{
Database = new DatabaseConfig
{
ConnectionString = "your-connection-string",
DatabaseType = DatabaseType.PostgreSQL
}
};
Summary of how to get the database ready and keep it up to date:
| Step | When | What to do |
|---|---|---|
| 1. Create DB | Once | Create an empty PostgreSQL or SQL Server database (see Database and connection). |
| 2. Connection string | Once | Set DATABASE_URL (or pass SubscrioConfig with Database.ConnectionString). |
| 3. Install schema | First run only | Call InstallSchemaAsync(adminPassphrase) to create all tables and seed the admin passphrase. |
| 4. Migrate | After library updates | Call MigrateAsync() to apply any new migrations. |
var subscrio = new Subscrio(config);
await subscrio.InstallSchemaAsync("your-secure-admin-passphrase");
This creates every required table and stores the hashed admin passphrase. Run it once per database.
var version = await subscrio.VerifySchemaAsync();
if (version == null)
{
await subscrio.InstallSchemaAsync("your-admin-passphrase");
}
Use this on startup if you want to install the schema only when it is missing.
After upgrading the Subscrio library, run migrations so the database schema stays in sync:
var migrationsApplied = await subscrio.MigrateAsync();
Console.WriteLine($"Applied {migrationsApplied} migrations");
Call MigrateAsync() during startup or as part of your deployment; it is safe to call when there are no pending migrations (it will apply zero and return 0).
// Create product
var product = await subscrio.Products.CreateProductAsync(new CreateProductDto(
Key: "saas-platform",
DisplayName: "SaaS Platform"
));
// Create features
var maxProjects = await subscrio.Features.CreateFeatureAsync(new CreateFeatureDto(
Key: "max-projects",
DisplayName: "Max Projects",
ValueType: FeatureValueType.Numeric,
DefaultValue: "10"
));
var ganttCharts = await subscrio.Features.CreateFeatureAsync(new CreateFeatureDto(
Key: "gantt-charts",
DisplayName: "Gantt Charts",
ValueType: FeatureValueType.Toggle,
DefaultValue: "false"
));
// Associate features with product
await subscrio.Products.AssociateFeatureAsync(product.Key, maxProjects.Key);
await subscrio.Products.AssociateFeatureAsync(product.Key, ganttCharts.Key);
// Create plan
var basicPlan = await subscrio.Plans.CreatePlanAsync(new CreatePlanDto(
ProductKey: product.Key,
Key: "basic",
DisplayName: "Basic Plan"
));
var proPlan = await subscrio.Plans.CreatePlanAsync(new CreatePlanDto(
ProductKey: product.Key,
Key: "pro",
DisplayName: "Pro Plan"
));
// Set feature values for plans
await subscrio.Plans.SetFeatureValueAsync(proPlan.Key, maxProjects.Key, "50");
await subscrio.Plans.SetFeatureValueAsync(proPlan.Key, ganttCharts.Key, "true");
// Create monthly billing cycle
var monthly = await subscrio.BillingCycles.CreateBillingCycleAsync(new CreateBillingCycleDto(
PlanKey: proPlan.Key,
Key: "pro-monthly",
DisplayName: "Pro Monthly",
DurationValue: 1,
DurationUnit: "months"
));
// Create yearly billing cycle
var yearly = await subscrio.BillingCycles.CreateBillingCycleAsync(new CreateBillingCycleDto(
PlanKey: proPlan.Key,
Key: "pro-yearly",
DisplayName: "Pro Yearly",
DurationValue: 1,
DurationUnit: "years"
));
// Create customer
var customer = await subscrio.Customers.CreateCustomerAsync(new CreateCustomerDto(
ExternalId: "customer-123",
DisplayName: "John Doe"
));
// Create subscription
var subscription = await subscrio.Subscriptions.CreateSubscriptionAsync(new CreateSubscriptionDto(
Key: "sub-001",
CustomerKey: customer.Key,
BillingCycleKey: monthly.Key
));
// Add feature override
await subscrio.Subscriptions.AddFeatureOverrideAsync(
subscription.Key,
maxProjects.Key,
"100",
OverrideType.Permanent
);
// Check if feature is enabled
var hasGanttCharts = await subscrio.FeatureChecker.IsEnabledAsync(
customer.ExternalId,
ganttCharts.Key
);
// Get feature value
var maxProjectsValue = await subscrio.FeatureChecker.GetValueAsync(
customer.ExternalId,
maxProjects.Key
);
// Get all features
var allFeatures = await subscrio.FeatureChecker.GetAllFeaturesAsync(customer.ExternalId);
Feature values are resolved using a smart hierarchy. See the main README for details.
Subscrio integrates with Stripe for payment processing. See the main README for an overview.
Important: Subscrio does NOT verify Stripe webhook signatures. You must verify signatures before passing events to Subscrio.
using Stripe;
// In your webhook endpoint (after verifying Stripe signature)
[HttpPost("webhooks/stripe")]
public async Task<IActionResult> HandleStripeWebhook()
{
var json = await new StreamReader(Request.Body).ReadToEndAsync();
var stripeSignature = Request.Headers["Stripe-Signature"].ToString();
try
{
// Verify webhook signature
var stripeEvent = EventUtility.ConstructEvent(
json,
stripeSignature,
_configuration["Stripe:WebhookSecret"]
);
// Process verified event
await _subscrio.Stripe.ProcessStripeEventAsync(stripeEvent);
return Ok(new { received = true });
}
catch (StripeException ex)
{
return BadRequest(new { error = ex.Message });
}
}
var subscription = await subscrio.Stripe.CreateStripeSubscriptionAsync(
customerExternalId: customer.ExternalId,
planKey: proPlan.Key,
renewalCycleKey: monthly.Key
);
Full .NET support with comprehensive type definitions and async/await patterns:
using Subscrio.Core;
using Subscrio.Core.Dtos;
// All APIs are fully typed
ProductDto product = await subscrio.Products.CreateProductAsync(new CreateProductDto(
Key: "my-product",
DisplayName: "My Product"
));
// DTOs are strongly typed
var feature = await subscrio.Features.CreateFeatureAsync(new CreateFeatureDto(
Key: "max-projects",
DisplayName: "Max Projects",
ValueType: FeatureValueType.Numeric,
DefaultValue: "10"
));
Keys vs IDs: All public APIs use keys (string identifiers like "my-product") rather than internal IDs. Keys are:
DTOs: All create/update operations use DTOs (Data Transfer Objects) with validation:
CreateProductDto, CreateFeatureDto, CreatePlanDto, etc.Async/Await: All operations are asynchronous:
Task<T> or Taskawait for all Subscrio operationsDependency Injection: Recommended for web applications:
AddSubscrio() extension methodServiceLifetime.Scoped for web appsAddSubscrio() in web applicationsServiceLifetime.Scoped for web apps to avoid tracking conflictsInstallSchemaAsync() once during application startupValidationException, NotFoundException, ConflictException appropriatelymax-projects)MIT License - see LICENSE file for details.
Contributions are welcome! Please see our Contributing Guide for details.