How to add subscription feature bundles in TypeScript with Subscrio
A wedding photographer uses VowGallery’s standard galleries for delivery. For some clients, the photographer buys a presentation bundle with custom branding and private selection tools. That purchase does not include more galleries.
In this guide we will put those two capabilities in one TypeScript add-on. We will attach and detach it, checking both feature decisions and the unchanged gallery allowance.
A subscription feature bundle groups optional capabilities into one add-on. This example implements the access granted by a confirmed purchase; payment collection happens before the application attaches the bundle.
Define the customer promise
| Decision | Standard purchase | With presentation bundle |
|---|---|---|
| Custom branding | No | Yes |
| Private selections | No | Yes |
| Gallery allowance | 20 | 20 |
Run the example
Get the complete runnable example. Install Node.js and PostgreSQL. The database role in DATABASE_URL must have permission to create databases.
git clone https://github.com/subscrio/samples.git
cd samples/examples/subscription-feature-bundles-typescript
cp .env.example .env
# Set DATABASE_URL in .env to your local PostgreSQL connection.
npm ci
npm test
In PowerShell, Copy-Item .env.example .env also copies the configuration template. Each run creates and removes its own disposable PostgreSQL database.
The snippets below belong to that single program and run in order. check compares a returned value with the expected value, prints it, and fails the run if they differ. The displayed output was captured from the sample.
Initialize Subscrio
Create the client with the disposable database connection, then install its schema.
const app = new Subscrio({
database: { connectionString: db.connectionString }
});
await app.installSchema();
Create the product
VowGallery has its own product catalog. The runner creates an isolated PostgreSQL database and removes only that generated database when it finishes.
await app.products.createProduct({
key: 'vowgallery',
displayName: 'VowGallery'
});
Define the features
Custom branding and private selections are independent yes-or-no decisions, so define them as toggles with false defaults. Gallery capacity is a number with a zero default. Keeping the values separate lets us prove that buying presentation tools does not buy storage capacity.
await app.features.createFeature({
key: 'custom-branding',
displayName: 'custom-branding',
valueType: 'toggle',
defaultValue: 'false'
});
await app.features.createFeature({
key: 'private-selections',
displayName: 'private-selections',
valueType: 'toggle',
defaultValue: 'false'
});
await app.features.createFeature({
key: 'gallery-limit',
displayName: 'gallery-limit',
valueType: 'numeric',
defaultValue: '0'
});
Associate the features
Associate all three features with VowGallery. The add-on rule combines the bundle with the plan: its enabled toggles can grant the optional features. The gallery limit stays at the plan value because this bundle contributes no gallery capacity.
await app.products.associateFeature('vowgallery', 'custom-branding', {
addonRule: 'additive',
subscriptionRule: 'most_generous'
});
await app.products.associateFeature('vowgallery', 'private-selections', {
addonRule: 'additive',
subscriptionRule: 'most_generous'
});
await app.products.associateFeature('vowgallery', 'gallery-limit', {
addonRule: 'additive',
subscriptionRule: 'most_generous'
});
Create the plan
Create Standard as the photographer’s base subscription plan. Its included features are separate from the optional Presentation bundle.
await app.plans.createPlan({
key: 'standard',
productKey: 'vowgallery',
displayName: 'standard'
});
Set the plan values
Standard includes twenty galleries. It leaves custom branding and private selections disabled until the Presentation add-on is attached.
await app.plans.setFeatureValue('standard', 'custom-branding', 'false');
await app.plans.setFeatureValue('standard', 'private-selections', 'false');
await app.plans.setFeatureValue('standard', 'gallery-limit', '20');
Create the billing cycle
This monthly catalog record is the subscription option. Creating it does not initiate a charge.
await app.billingCycles.createBillingCycle({
key: 'standard-monthly',
planKey: 'standard',
displayName: 'Monthly',
durationUnit: 'months',
durationValue: 1
});
Create the customer
Create the customer receiving this agreement.
await app.customers.createCustomer({
key: 'customer',
displayName: 'VowGallery demo customer'
});
Assign the standard subscription
The agreement subscription connects customer to standard-monthly and the Standard plan. The customer now receives twenty galleries and neither presentation feature.
await app.subscriptions.createSubscription({
key: 'agreement',
customerKey: 'customer',
billingCycleKey: 'standard-monthly'
});
Define the feature-bundle add-on
This package contains only the two toggles. It has no gallery-limit contribution.
await app.addons.createAddon({
key: 'presentation',
productKey: 'vowgallery',
displayName: 'Client presentation',
featureValues: { 'custom-branding': 'true', 'private-selections': 'true' }
});
Read the three decisions
The getter for each toggle answers whether its action is included. The numeric getter supplies the total gallery allowance. Each snapshot asserts the full combination.
async function snapshot(label: string, enabled: boolean) {
const branding = await app.featureChecker.isEnabledForCustomer(
'customer',
'vowgallery',
'custom-branding'
);
const selections = await app.featureChecker.isEnabledForCustomer(
'customer',
'vowgallery',
'private-selections'
);
const galleries = await app.featureChecker.getValueForCustomer(
'customer',
'vowgallery',
'gallery-limit',
0
);
assert.deepEqual([branding, selections, galleries], [enabled, enabled, 20]);
console.log(
label + ': ' + JSON.stringify({ branding, selections, galleries })
);
}
await snapshot('Before purchase', false);
Captured output:
Before purchase: {"branding":false,"selections":false,"galleries":20}
Attach the subscription add-on to enable both features
One attachment makes both customer feature checks true. No second plan or second purchase is needed.
await app.subscriptions.attachAddon('agreement', 'presentation');
await snapshot('Bundle attached', true);
Captured output:
Bundle attached: {"branding":true,"selections":true,"galleries":20}
Remove the feature bundle and verify base access
Both toggles return to their base values, while the numeric allowance remains twenty.
await app.subscriptions.detachAddon('agreement', 'presentation');
await snapshot('Bundle detached', false);
Captured output:
Bundle detached: {"branding":false,"selections":false,"galleries":20}
Read the result
The bundle changes both presentation decisions together. The gallery allowance remains twenty before, during, and after attachment. The numeric getter returns a limit, not the number of galleries already stored.
A package should name its benefits precisely. Keeping gallery capacity outside this bundle makes the commercial boundary testable: buying or removing presentation tools cannot silently change the number of galleries included. The application still needs to use each returned toggle when accepting the corresponding action.
The Subscrio add-on guide explains the related model.
Run the complete TypeScript example to reproduce the entitlement decisions and their expected results.