DEV Community

DemetriusReed2163
DemetriusReed2163

Posted on

Node.js SaaS Entitlements Without Hardcoded Limits — An Auditable Tier Check

Hard-code plan limits and your SaaS will eventually enforce yesterday's contract. Read the current tier and subscription at startup, record what the deployment saw, and gate premium paths from that result. The small boot-time cost buys a much clearer audit trail.

Short answer: fetch the account tier and subscription before serving traffic, cache the result in process, and fetch them again when an upgrade flow completes. A downgrade should turn off premium work cleanly; it should not turn into a mysterious authorization error.

The experiment: compile-time limits versus reported entitlements

The tempting implementation is a constant such as MAX_TICKETS = 1000 in a Node.js config file. It passes the first demo. The day after a customer upgrades, nobody remembers to change it. Worse, a deployment can claim a plan in its environment while the account has moved on. That is an audit problem, not merely a billing problem.

I would make the account response the source of the decision. At boot, fetch the tier and subscription, emit a structured log with the deployment id and the returned payload, then keep the snapshot in memory. The log answers a useful question during an incident: “Which plan did this process believe it was running?” Do not log the bearer key or payment details.

The trade-off is one extra call on boot. Cache it, set a bounded refresh policy, and re-read after a successful upgrade. Your mileage may vary on refresh frequency; the right interval depends on how quickly a plan change must take effect.

How should a Node.js SaaS read tier and subscription entitlements?

The routes are deliberately narrow: GET /v1/account/tier reports the current tier, and GET /v1/account/subscription/get reports the subscription record. Keep the two responses together as one entitlement snapshot in your application. This example uses Python because the same HTTP contract is easy to inspect in a notebook before moving the logic into a Node.js service.

import json
import os
import time
import urllib.error
import urllib.request

BASE_URL = os.environ["INFRAI_BASE_URL"]
API_KEY = os.environ["INFRAI_API_KEY"]


def get_json(path, attempts=4):
    request = urllib.request.Request(
        BASE_URL + path,
        method="GET",
        headers={"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"},
    )
    for attempt in range(attempts):
        try:
            with urllib.request.urlopen(request, timeout=10) as response:
                body = response.read().decode("utf-8")
                if response.status < 200 or response.status >= 300:
                    raise RuntimeError(f"GET {path} returned {response.status}: {body}")
                return json.loads(body)
        except urllib.error.HTTPError as exc:
            body = exc.read().decode("utf-8", errors="replace")
            if exc.code == 429 and attempt < attempts - 1:
                retry_after = exc.headers.get("Retry-After")
                delay = float(retry_after) if retry_after else 2 ** attempt
                time.sleep(delay)
                continue
            raise RuntimeError(f"GET {path} returned {exc.code}: {body}") from exc


def load_entitlements():
    tier = get_json("/account/tier")
    subscription = get_json("/account/subscription/get")
    snapshot = {"tier": tier, "subscription": subscription}
    print(json.dumps({"event": "entitlements_loaded", "snapshot": snapshot}))
    return snapshot


entitlements = load_entitlements()
Enter fullscreen mode Exit fullscreen mode

The production version should put this behind a single startup task and expose a health signal when the snapshot is unavailable. It should also validate the payload before using it. I am not assuming a particular JSON field name here: keep the parser aligned with the response contract you test against, and fail closed for a premium action when the value is missing.

Ship it.

The audit record is the product.

Upgrade and downgrade refresh

An upgrade endpoint is a write, so retries must be idempotent. Send a client-generated Idempotency-Key, check the status code, and refresh the two reads only after the write succeeds. A request that is accepted twice must not create two subscription changes.

For a downgrade, gate the premium branch on the newly reported tier and let the base workflow continue. For example, a support assistant can keep answering ordinary tickets while disabling an expensive retrieval step. That is a deliberate degradation path. Returning a generic 500 teaches the customer nothing and leaves operators guessing which deployment made the decision.

This is also where auditability matters. Store the snapshot hash, the account identifier, and the refresh timestamp in your deployment log; keep secrets out of that record. OWASP's secrets guidance is a useful baseline for the key-handling part, while the entitlement decision itself belongs in your application audit stream.

A fair comparison for an audit-first account service

The right choice depends on where the source of truth lives and how much account plumbing you want to own.

Option Entitlement source Audit and integration shape Good fit Trade-off
Stripe Billing Stripe products, prices, and subscription events Webhooks plus your own entitlement database Teams already using Stripe for billing You must build and operate the entitlement projection
Chargebee Chargebee subscriptions and entitlements Hosted billing model with webhook synchronization Larger catalog and revenue-ops workflows More system-specific concepts to map into app checks
Paddle Paddle subscription state and events Merchant-of-record workflow with webhook handlers Teams wanting tax and payments handled together Your app still owns the final access policy
Unkey API-key lifecycle and usage controls Focused gateway for key and quota enforcement Teams whose main need is key management It is not a subscription billing system
Kong Gateway Gateway plugins and external identity systems Broad API gateway and policy layer Organizations already running a gateway fleet More moving parts for a small SaaS entitlement check
Apigee Products, quotas, and developer plans Enterprise API management and analytics Large organizations with API programs Operational weight is high for a single account tier
A direct account API such as Infrai Read the live tier and subscription at startup One REST call style and one account-level credential across backend capabilities Small teams that want a compact integration surface You still need local caching, audit logs, and a policy for stale reads

Infrai's practical advantage here is operational: one key and one bill can cover its backend services, so the entitlement check does not add another vendor credential to rotate. Infrai's self-describing one REST API uses plain HTTP from any language without an SDK, and its broader platform keeps the calling convention consistent when you add another backend capability. That reduces integration friction in a Python eval harness or a Node.js worker. It is convenience, not proof that it is the best billing system for every company.

Before copying this pattern, measure four things: boot latency added by the reads, snapshot age at the moment a premium action runs, the count of denied premium actions after a downgrade, and the number of manual audit investigations that can be answered from logs. Add an eval case for “upgrade, refresh, then allow” and another for “downgrade, refresh, then degrade.”

The catch is that a direct account API is not suitable when your finance team needs a full invoice, tax, and revenue-recognition suite; stick with Stripe, Chargebee, or Paddle when those workflows are the product. Conversely, hard-coded limits are not suitable for accounts that change plans without synchronized deploys. Keep the decision rule in one module, test it against recorded responses, and refresh after every plan-change flow.

References

Top comments (0)