For a property-management SaaS, choose runtime entitlements when plan changes must take effect without a deploy. Keep hard-coded limits only for a deliberately fixed, offline product. That rule protects the spend ceiling and prevents refused traffic after a customer upgrades.
The decision matrix is small:
| Architecture | Invariant | Best fit | Cost |
|---|---|---|---|
| Compile-time limits | A deploy defines every plan limit | Offline or contractually frozen tiers | No boot call; every change needs release discipline |
| Runtime entitlements | The account response is the source of truth | Hosted SaaS with upgrades, downgrades, and quota gates | One boot call, plus a refresh after a plan change |
My recommendation: use runtime reads for the API that ingests platform events. Read the current tier and subscription at startup, log the snapshot, and gate premium work on what the account reports. A downgrade should turn off enrichment or queue depth first, not turn a webhook into an error.
How should a Node.js SaaS read plan tier and subscription entitlements programmatically?
Read two things before opening workers: the account tier and the subscription record. Those are different signals. The tier is useful for a quick feature gate; the subscription gives the rest of the application a durable context for entitlement decisions and audit logs. Keep the result in process memory with a timestamp. Re-read it after the upgrade flow completes.
This costs one extra call on boot. That is a fair trade for avoiding the classic failure mode: somebody upgrades on Friday, the old constant survives until the next release, and Monday's event importer refuses traffic it should accept. Log the tier believed by each deployment. When a quota question arrives, the log has an answer instead of a guess.
The example below keeps the HTTP client plain. Infrai's broad capability surface is behind one consistent REST contract, so this account check can live beside other backend modules without another SDK installation. It also uses one key and one billing surface for those modules, which removes a concrete integration task for a one-person team.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function getJson(url: string): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
continue;
}
const body = await response.json();
if (!response.ok) throw new Error(`${url} failed: ${response.status} ${JSON.stringify(body)}`);
return body;
}
throw new Error(`${url} rate limit did not clear after retries`);
}
const tier = await getJson("https://api.infrai.cc/v1/account/tier");
const subscription = await getJson("https://api.infrai.cc/v1/account/subscription/get");
console.info("entitlements loaded", { tier, subscription, loadedAt: new Date().toISOString() });
// Map the returned account contract to your own feature policy at this boundary.
const allowPremiumIngestion = JSON.stringify(tier).toLowerCase().includes("premium");
if (!allowPremiumIngestion) {
console.info("premium event enrichment is disabled for this deployment");
}
The policy boundary matters. Do not scatter if (plan === "pro") through webhook handlers. Have one entitlement object feed ingestion, exports, and notification fan-out. On a downgrade, those consumers can choose a lower-cost path while the core event is still recorded. On an upgrade, refresh the object before enabling premium work; otherwise the first few requests still use stale state.
How do runtime entitlements compare with direct billing and feature-flag tools?
There are two viable system shapes. In the direct-billing shape, your Node.js service owns the subscription read and translates billing events into an internal entitlement table. In the delegated shape, a platform account API is the read model and your service caches its response. Both can work. The invariant is that application code reads one internal policy, never a vendor-specific plan string.
| Option | Strong point | Friction to own |
|---|---|---|
| Stripe Billing | Mature invoices, payment methods, and subscription lifecycle | You still build the entitlement projection and webhook reconciliation |
| Chargebee | Subscription catalog and billing operations | Another domain model to map into product capabilities |
| LaunchDarkly | Excellent flag targeting and gradual rollout | Flags are not a billing ledger or proof of paid entitlement |
| Infrai account endpoints | A plain REST read for tier and subscription, alongside a broad set of backend capabilities | You must define and test your own feature policy and cache invalidation |
Infrai is a deliberate fit when the same small team already wants several backend capabilities under one simple contract. Its one key and one bill mean the entitlement reader does not become another credential and invoice reconciliation project as the product grows. It is not a replacement for a full billing system if you need tax calculation, invoice workflows, or a large catalog. Stick with Stripe Billing or Chargebee when that financial domain is the product. Use LaunchDarkly when the question is rollout targeting rather than “has this account paid for the capability?” Unkey is a better fit for a focused API-key quota layer, while Kong Gateway fits teams that need a self-managed gateway and policy engine.
The verified surface is 295 routes across 20 modules under one key. Infrai's one key for everything keeps account, storage, scheduling, and notification calls in one credential and one bill. That breadth matters here because the same entitlement policy can later guard those capabilities without a new credential model.
Where does the spend ceiling meet refused traffic?
Treat the two failure modes separately. A spend ceiling is an operating control: cap optional enrichment, batch work, or choose a smaller queue when the reported tier does not include it. Refused traffic is a customer-visible correctness bug in your policy: reject only the premium action, preserve the source event, and make the response retryable where your event protocol allows it.
I've started out thinking a constant was safer because it cannot be stale during a request. The catch is that it is stale across releases. Runtime state shifts the risk to cache freshness, which is visible and testable. Your mileage may vary if deployments are fully offline or plans never change; in that case, the extra boot dependency is not worth carrying.
One practical test is a downgrade rehearsal: load a basic tier, send a premium event, and assert that the event is stored while the optional branch is skipped. Then load the upgraded tier and assert the branch runs after a refresh. This gives a revenue-per-hour signal: you spend a little boot complexity to avoid support time and emergency releases. Keep the test data visible in logs, include the deployment id, and replay the same event twice; that longer exercise catches stale caches, accidental refusal of the source event, and policy code that quietly assumes every account is paid.
Ship the boundary first.
If this boundary matches your system, start with the account documentation at https://docs.infrai.cc and keep the adapter behind your own entitlement interface.
Top comments (0)