How to grant buy-once app access in TypeScript with Subscrio
RidgeAtlas sells its complete hiking planner for one payment. A purchaser can draw routes, inspect elevation profiles, and build itineraries. When that customer moves from a laptop to a phone, the purchase should follow them instead of prompting for another payment.
In this guide we will model a one-time app purchase in TypeScript and use one customer entitlement for every planning tool, including a later visit from another device.
This Node.js example starts after payment confirmation. The one-time purchase grants a non-expiring customer entitlement for the whole application. Later visits reuse that access once the application identifies the same customer.
Define the customer promise
| Visit | Route drawing | Elevation | Itinerary |
|---|---|---|---|
| Customer before purchase | Denied | Denied | Denied |
| Purchaser on laptop | Available | Available | Available |
| Same purchaser on phone later | Available | Available | Available |
Run the example
The complete RidgeAtlas sample contains the setup and executable assertions used below. Use Node.js and a local PostgreSQL server. Copy the environment example and set DATABASE_URL to a development connection with permission to create databases. The runner creates and removes its own isolated database.
git clone https://github.com/subscrio/samples.git
cd samples/examples/one-time-app-purchase-typescript
cp .env.example .env
# Set DATABASE_URL in .env before running.
npm ci
npm test
The snippets run in order within that program. check prints the returned value and asserts that it matches the expected result. The outputs below come from the executable sample.
Initialize Subscrio
The helper supplies the disposable PostgreSQL database connection. Install the schema before creating any catalog records.
const clock: { at: Date | null } = { at: null };
const app = new Subscrio({
database: { connectionString: db.connectionString },
clock: { now: () => clock.at ?? new Date() }
});
await app.installSchema();
The full runner supplies the imports, database helper, and cleanup. Its injectable clock lets the boundary checks advance time without waiting. It controls Subscrio’s entitlement/accounting evaluation; it does not change the database server clock.
Create the product
Keep RidgeAtlas’s catalog under one product.
await app.products.createProduct({
key: 'ridgeatlas',
displayName: 'RidgeAtlas'
});
Define the feature
The app-access toggle covers route drawing, elevation profiles, and itineraries together. Its false default keeps the planner closed to customers who have not purchased it.
await app.features.createFeature({
key: 'app-access',
displayName: 'app-access',
valueType: 'toggle',
defaultValue: 'false'
});
Associate the feature
Associate this feature with RidgeAtlas so its plan can supply the customer’s value.
await app.products.associateFeature('ridgeatlas', 'app-access', {
subscriptionRule: 'most_generous'
});
Create the plan
Create one plan for the complete RidgeAtlas application. Route drawing, elevation profiles, and itineraries will all use its app-access entitlement.
await app.plans.createPlan({
key: 'standard',
productKey: 'ridgeatlas',
displayName: 'standard'
});
Assign the plan value
The purchase plan enables the entire planner with one true value.
await app.plans.setFeatureValue('standard', 'app-access', 'true');
Create the billing cycle
The forever cycle represents a non-expiring agreement. It creates no recurring charge.
await app.billingCycles.createBillingCycle({
key: 'standard-cycle',
planKey: 'standard',
displayName: 'Ongoing',
durationUnit: 'forever'
});
Create the customer
The sample uses the key customer for the RidgeAtlas purchaser. Creating this record identifies the recipient; the subscription will assign their plan.
await app.customers.createCustomer({
key: 'customer',
displayName: 'RidgeAtlas customer'
});
Grant whole-app access after the one-time purchase
This fixture represents an order whose payment has already been verified. The purchase creates a non-expiring agreement; it does not start a recurring charge.
await app.subscriptions.createSubscription({
key: 'purchase-ridge-204',
customerKey: 'customer',
billingCycleKey: 'standard-cycle'
});
Create a customer without a purchase
The visitor customer has no subscription. We will use it to verify that the same planner action returns no paid tools before purchase.
await app.customers.createCustomer({
key: 'visitor',
displayName: 'Customer without a purchase'
});
Use one entitlement for the whole planner
The three tools share app-access. Device names below only identify test visits; they do not affect the entitlement.
async function openPlanner(customer: string, device: string) {
const purchased = await app.featureChecker.getValueForCustomer(
customer,
'ridgeatlas',
'app-access',
false
);
return {
device,
tools: purchased ? ['route', 'elevation', 'itinerary'] : []
};
}
check('Laptop tools', (await openPlanner('customer', 'laptop')).tools, [
'route',
'elevation',
'itinerary'
]);
check('Visitor tools', (await openPlanner('visitor', 'phone')).tools, []);
Output:
Laptop tools: ["route","elevation","itinerary"]
Visitor tools: []
Reuse the customer entitlement on a later visit
A year later, the same customer still resolves the original purchase. No second agreement or payment is created for the phone.
clock.at = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000);
check(
'Phone tools on later visit',
(await openPlanner('customer', 'phone')).tools,
['route', 'elevation', 'itinerary']
);
const purchase = await app.subscriptions.getSubscription('purchase-ridge-204');
check('Original purchase customer', purchase?.customerKey, 'customer');
Output:
Phone tools on later visit: ["route","elevation","itinerary"]
Original purchase customer: "customer"
This is a whole-app purchase, not a region pack or a credit balance. Refunds and revocations are separate business decisions; the example demonstrates access with no scheduled renewal.
Use the result in your application
The purchase belongs to the customer, so moving to another device does not change which tools are available. The original agreement still provides route drawing, elevation profiles, and itineraries on the later visit. There is no recurring credit allocation or region-specific add-on to maintain.
Run the complete RidgeAtlas example to reproduce the checks. The Subscrio subscription feature gating guide explains the related model.
For a related C# example, see a whole-app purchase in ASP.NET Core. A related next step is prepaid rendering jobs in TypeScript.