<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Subscrio]]></title><description><![CDATA[Practical C# and TypeScript guides to application feature access, entitlements, usage limits, and prepaid credits from the team building Subscrio.]]></description><link>https://subscrio.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ac40bf9e2c9dfd0ddaa7f56/ec22f095-b0ef-4aa6-b0f6-4e0832b6b2cd.png</url><title>Subscrio</title><link>https://subscrio.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 10 Oct 2026 06:50:21 GMT</lastBuildDate><atom:link href="https://subscrio.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Enforce paid feature access on the server in TypeScript]]></title><description><![CDATA[Hiding a paid feature's button helps customers understand their plan, but it does not prevent someone from calling the server directly. If the server trusts the screen to enforce access, a request tha]]></description><link>https://subscrio.hashnode.dev/enforce-paid-feature-access-on-the-server-in-typescript</link><guid isPermaLink="true">https://subscrio.hashnode.dev/enforce-paid-feature-access-on-the-server-in-typescript</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Jasen Fici]]></dc:creator><pubDate>Mon, 05 Oct 2026 21:48:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac40bf9e2c9dfd0ddaa7f56/7e9cc61c-bb6c-4ae0-ad3c-a13de9e8ea0b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hiding a paid feature's button helps customers understand their plan, but it does not prevent someone from calling the server directly. If the server trusts the screen to enforce access, a request that skips the screen can perform an operation the customer has not bought.</p>
<p>In this guide we will put the entitlement check inside a TypeScript publication action, before it records any work. You'll configure two plans, send direct HTTP requests for each customer, and verify both the denial and the successful publication.</p>
<p>I build <a href="https://subscrio.com">Subscrio</a>, the entitlement library used here. It resolves whether a customer has a feature; the application must use that answer before performing the operation.</p>
<h2>Before you get started</h2>
<p>The complete <a href="https://github.com/subscrio/samples/tree/main/examples/server-side-feature-gating-typescript">BracketCamp sample</a> is in the public <a href="https://github.com/subscrio/samples">Subscrio samples repository</a>. To run it, you'll need:</p>
<ul>
<li><p><a href="https://nodejs.org/en/download">Node.js and npm</a> to install dependencies and run the TypeScript example.</p>
</li>
<li><p>A local <a href="https://www.postgresql.org/download/">PostgreSQL server</a> and a development role that can create databases. Each test run creates and removes its own temporary database.</p>
</li>
<li><p><a href="https://git-scm.com/downloads/">Git</a> to clone the repository.</p>
</li>
<li><p>The <a href="https://www.npmjs.com/package/subscrio">Subscrio npm package</a>, which the sample's <code>npm ci</code> command installs along with its other pinned dependencies.</p>
</li>
</ul>
<p>The snippets follow one program's main flow. The linked sample includes its imports, database setup, and test helpers; the excerpts are not separate runnable programs.</p>
<h2>The request that bypasses the button</h2>
<p>BracketCamp is a fictional app for tournament organizers. Organizers use it to track matches; its Broadcast plan also lets them publish a live bracket so spectators can follow results. The Club plan keeps tournaments private.</p>
<p>Suppose the Club screen hides Publish, but the publication handler accepts every request it receives. An organizer can still send the same HTTP request directly. The problem is where the decision happens: hiding the button has not protected the publication action.</p>
<p>We'll test a Club customer named <code>local-club</code> and a Broadcast customer named <code>regional-open</code>. A direct request from Club should return HTTP 403 and publish nothing. Broadcast should receive HTTP 201 and have its publication recorded. Blocking both requests would fail the product's promise too.</p>
<h2>Make the publication rule available to the server</h2>
<p>The <code>live-brackets</code> feature will answer one question: can this customer publish a bracket for spectators? It defaults to false, and only Broadcast grants it. The following setup connects that rule to the two customer subscriptions before we write the action that uses it.</p>
<h2>Connect Subscrio to the sample database</h2>
<p>The sample calls <code>isolatedDatabase</code>, a helper defined in <code>src/database.ts</code>, to create a temporary PostgreSQL database. Its returned <code>db.connectionString</code> is passed to Subscrio. <code>installSchema</code> creates the tables that will hold the catalog and subscriptions.</p>
<pre><code class="language-typescript">const app = new Subscrio({ database: { connectionString: db.connectionString } });
await app.installSchema();
</code></pre>
<h2>Register the tournament app</h2>
<p>Create the <code>bracketcamp</code> product to group this app's plans and features.</p>
<pre><code class="language-typescript">await app.products.createProduct({ key: 'bracketcamp', displayName: 'BracketCamp' });
</code></pre>
<h2>Define permission to publish a live bracket</h2>
<p>A toggle represents this yes-or-no capability. The default <code>"false"</code> means a customer needs a qualifying subscription before publishing. Feature values are strings in the catalog API.</p>
<pre><code class="language-typescript">await app.features.createFeature({
  key: 'live-brackets',
  displayName: 'live-brackets',
  valueType: 'toggle',
  defaultValue: 'false',
});
</code></pre>
<h2>Make the feature part of BracketCamp</h2>
<p>Associate <code>live-brackets</code> with the product so its plans can assign a value.</p>
<pre><code class="language-typescript">await app.products.associateFeature('bracketcamp', 'live-brackets');
</code></pre>
<h2>Create the private and public offerings</h2>
<p>Club provides private tournament management. Broadcast includes publication for spectators. Create each plan under the same product.</p>
<pre><code class="language-typescript">for (const key of ['club', 'broadcast'])
  await app.plans.createPlan({ key, productKey: 'bracketcamp', displayName: key });
</code></pre>
<h2>Allow publication only on Broadcast</h2>
<p>Translate the commercial rule into feature values. Both subscriptions can use the product, but only Broadcast enables <code>live-brackets</code>.</p>
<pre><code class="language-typescript">await app.plans.setFeatureValue('club', 'live-brackets', 'false');
await app.plans.setFeatureValue('broadcast', 'live-brackets', 'true');
</code></pre>
<h2>Add monthly subscription options</h2>
<p>Each subscription must reference a billing cycle belonging to a plan. These monthly records provide that link; creating them does not collect payment.</p>
<pre><code class="language-typescript">for (const planKey of ['club', 'broadcast'])
  await app.billingCycles.createBillingCycle({
    key: planKey + '-monthly',
    planKey,
    displayName: 'Monthly',
    durationUnit: 'months',
    durationValue: 1,
  });
</code></pre>
<h2>Create the two tournament customers</h2>
<p>Register the tournament businesses separately so we can prove that the handler treats their subscriptions differently.</p>
<pre><code class="language-typescript">await app.customers.createCustomer({ key: 'local-club' });
await app.customers.createCustomer({ key: 'regional-open' });
</code></pre>
<h2>Connect each customer to the plan they bought</h2>
<p><code>club-agreement</code> connects <code>local-club</code> to <code>club-monthly</code>; <code>broadcast-agreement</code> connects <code>regional-open</code> to <code>broadcast-monthly</code>. The publication action will receive the customer key and let Subscrio resolve the feature through this relationship.</p>
<pre><code class="language-typescript">await app.subscriptions.createSubscription({
  key: 'club-agreement',
  customerKey: 'local-club',
  billingCycleKey: 'club-monthly',
});
await app.subscriptions.createSubscription({
  key: 'broadcast-agreement',
  customerKey: 'regional-open',
  billingCycleKey: 'broadcast-monthly',
});
</code></pre>
<h2>Check access before recording a publication</h2>
<p>Now place the decision inside the action reached by the HTTP handler. <code>publish</code> is our application function. <code>isEnabledForCustomer</code> is the Subscrio API: it returns whether this customer has <code>live-brackets</code> in <code>bracketcamp</code>.</p>
<p>A false result returns a 403 response before the application records a publication. A true result records the customer and returns 201. The <code>published</code> array is a stand-in for accepted work so we can prove the denied request had no effect.</p>
<pre><code class="language-typescript">const published: string[] = [];
async function publish(customerKey: string) {
  const allowed = await app.featureChecker.isEnabledForCustomer(
    customerKey,
    'bracketcamp',
    'live-brackets',
  );
  if (!allowed) return { status: 403, body: { error: 'live_brackets_not_included' } };
  published.push(customerKey);
  return { status: 201, body: { status: 'published' } };
}
</code></pre>
<p>A production handler would create the actual bracket publication after the same check. It must also establish which customer the authenticated organizer represents; the test routes below use fixed customer keys only to exercise the entitlement decision.</p>
<h2>Send requests that never touch the screen</h2>
<p>The sample's <code>replayPublicationRequests</code> helper starts a temporary HTTP server on loopback, sends both requests with Node's <code>fetch</code>, verifies their status and JSON responses, and closes the server. It is test code in <a href="https://github.com/subscrio/samples/blob/main/examples/server-side-feature-gating-typescript/src/http-check.ts"><code>src/http-check.ts</code></a>, not a Subscrio API. No request body or authentication header is needed for these fixed local test routes.</p>
<p>In the two calls below, <code>baseUrl</code> stands for the temporary server's address and port. They illustrate the requests the helper sends for each customer:</p>
<pre><code class="language-typescript">await fetch(baseUrl + '/customers/local-club/public-bracket', { method: 'POST' });
await fetch(baseUrl + '/customers/regional-open/public-bracket', { method: 'POST' });
</code></pre>
<p>The main program passes our publication function to that helper. Node's built-in <code>assert.deepEqual</code> then checks the accepted-work array, so a denied request cannot silently publish anyway.</p>
<pre><code class="language-typescript">await replayPublicationRequests(publish);
assert.deepEqual(published, ['regional-open']);
</code></pre>
<p>Captured responses:</p>
<pre><code class="language-text">local-club: HTTP 403 {"error":"live_brackets_not_included"}
regional-open: HTTP 201 {"status":"published"}
</code></pre>
<p>Club reached the server directly but could not publish. Broadcast succeeded, and <code>published</code> contains only <code>regional-open</code>. The restriction now holds even when a caller never opens the screen.</p>
<h2>Run the complete example</h2>
<p>The <a href="https://github.com/subscrio/samples/tree/main/examples/server-side-feature-gating-typescript">BracketCamp sample</a> contains the complete program, imports, database helper, and assertions for the results shown here. Use Node.js and a local PostgreSQL server. Copy <code>.env.example</code> to <code>.env</code> and set <code>DATABASE_URL</code> to a development connection whose role can create databases. Keep that file private.</p>
<pre><code class="language-bash">git clone https://github.com/subscrio/samples.git
cd samples/examples/server-side-feature-gating-typescript
cp .env.example .env
# Set DATABASE_URL in .env before continuing.
npm ci
npm test
</code></pre>
<p>Each run creates its own disposable database and removes it afterward. The snippets above follow the program's main flow; the database helper and final assertions live in the linked source. The assertions fail the run if the observed results differ from the expected customer behavior.</p>
<p>AI tools assisted with drafting and editing this article. The sample code and its expected behavior were checked separately through executable tests.</p>
]]></content:encoded></item></channel></rss>