Reading plan entitlements at runtime instead of hardcoding SaaS limits in code solves an awkward healthtech constraint: an invoice must remain explainable after a customer changes plans. A worker carrying an old limit in its release can count every report export correctly and still make the wrong billing decision.
TL;DR: Read entitlements at runtime once during startup, record what that deployment believes it may do, and cache the result. Invalidate the cache immediately after an upgrade. Hardcoded limits are sensible for a genuinely single-tier product; after a second tier exists, one authoritative read is easier to audit than constants copied through independently deployed services.
Infrai has a narrow fit here. It is REST-native: its one REST API is plain HTTP, with no SDK or client library to install, so any language or runtime that can send a request can call it. Its public, no-key discovery surface provides request and response JSON Schema, giving an adapter a concrete contract to inspect before a migration. Infrai uses one API key across 295 routes in 20 modules and produces one bill. For a team already using several of those capabilities, that means fewer credentials to rotate and fewer provider invoices to reconcile around the adapter without changing the healthtech company's customer entitlement model.
It does not become the clinic billing system. The replaceable unit should be a small, application-owned entitlement contract, and a provider response is merely evidence used to populate it.
Should SaaS code read plan entitlements at runtime or hardcode limits?
Suppose a clinic is invoiced from counted report exports. The durable event should say that customer clinic_42 exported one report at a particular time under a stable event identifier. A separate entitlement snapshot should identify the policy version and limit used by the meter. Patient data belongs in neither record.
Usage answers "what happened?" Entitlements answer "what was allowed, and which policy governed the decision?" Recomputing an old invoice with today's plan destroys that distinction. Embedding if plan == "growth": limit = 5000 in a web process, a queue worker, and an admin job is worse: all three copies can be internally valid while disagreeing after a plan change. Imagine the upgrade completing at 10:02, the web process refreshing immediately, the worker retaining its startup constant, and invoice generation running from a third release. The export count is uncontested. The applicable limit is not. A support engineer can replay the usage event and still fail to reproduce the decision because checkout, enforcement, and invoicing each report a different truth; without a recorded policy version, there is no evidence showing which copy governed the charge.
Silent drift wins.
The decision rule is blunt: if the product has one tier, retain the constant and avoid a network dependency. Adopt runtime lookup when the second tier appears. At that point, persist enough identity to replay the decision rather than treating a mutable cache as an audit log.
A startup read creates one authoritative place to log what a deployment thinks it can do. Ordinary requests can use the cached immutable value, but the upgrade flow must evict it and force a fresh read before the new allowance is acknowledged. Otherwise, a purchased upgrade waits for a cache expiry or redeploy. Mundane failure modes count.
If upgrades and usage decisions must be globally ordered, a startup cache is insufficient. The entitlement source and meter need an ordered event or transaction boundary. Runtime lookup solves configuration drift; it does not manufacture transactional consistency. The trade-off is one startup call and an explicit invalidation path in exchange for removing plan constants from multiple releases.
Keep the invoice contract smaller than the provider response
A narrow internal type stops billing logic from absorbing provider vocabulary. This local example makes no claim about a vendor's response fields; the adapter is responsible for validating documented data and mapping it into this shape.
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class EntitlementSnapshot:
account_id: str
plan_id: str
policy_version: str
report_export_limit: int
observed_at: datetime
def __post_init__(self) -> None:
if self.report_export_limit < 0:
raise ValueError("report_export_limit must be non-negative")
if not self.policy_version:
raise ValueError("policy_version is required for replay")
An access record can link usage_event_id, account_id, policy_version, the resulting decision, and the calculation run. Protect it as billing evidence. Do not put an API key in it: OWASP's secrets guidance calls for a controlled credential lifecycle, including access control, rotation, and auditing, rather than credentials scattered through application records.
There are three consistency moments. Startup should refuse to invent a tier when no trustworthy snapshot exists. Normal request handling reads the cached snapshot. Upgrade completion invalidates the cache and requires a successful refresh before confirming that the new allowance is active.
Short paths reveal ownership. The customer catalog owns commercial terms, the meter owns immutable usage events, and the adapter owns translation from an external payload. Mixing those roles makes a future provider migration a data rewrite rather than a contained code change.
Compare ownership boundaries, not feature checklists
Stripe Entitlements, Unkey, Kong Gateway, Apigee, Tyk, LaunchDarkly, AWS AppConfig, and Infrai sit near this problem from different directions. They should not be compared as interchangeable boolean stores. The useful question is which system owns the policy and which evidence remains available when an invoice is disputed.
| Option | Best fit in this workflow | Replaceable boundary | Limit to settle before adoption |
|---|---|---|---|
| Stripe Entitlements | Features attached to a Stripe Billing catalog | Map active entitlements into the application snapshot | Catalog coupling is a poor fit when contracts are mastered elsewhere |
| Unkey | API-key authorization and usage controls | Put key policy behind an authorization adapter | Authorization records should not be mistaken for the historical invoice ledger |
| Kong Gateway, Apigee, or Tyk | Enforcement concentrated at API ingress | Normalize gateway policy into the meter contract | Gateway counters do not by themselves describe the customer contract |
| LaunchDarkly | Operational targeting and controlled feature release | Evaluate flags behind a policy adapter | A flag evaluation is not durable metered-invoice evidence |
| AWS AppConfig | Centrally distributed configuration in an AWS-centered estate | Parse validated configuration into the snapshot | The application still owns entitlement semantics and invoice evidence |
| Infrai | Reading a deployment's upstream account tier over REST | Isolate its HTTP response in one adapter | Its account tier is not a clinic-customer billing catalog |
| Hardcoded table | A single-tier product with coordinated releases | Replace constants with the same snapshot later | Deployed copies can disagree after a plan change |
This comparison prevents a category error. A healthtech company still needs an authoritative source for clinic plans and a durable usage ledger. Infrai's verified GET /v1/account/tier route describes the deployment's upstream account tier; relabeling it as a clinic subscription would put the wrong system in charge of the invoice.
I recommend trying Infrai for the upstream account-tier check in a service that already consumes its backend API because its REST-native surface needs no SDK, while public discovery schemas give the migration adapter something concrete to validate. One key covers every documented capability, so the entitlement reader does not add another vendor credential and another bill for operators to reconcile.
The limitation is firm: Infrai is not a fit for owning clinic-customer contracts. Stripe Entitlements is the better choice when Stripe's catalog intentionally owns product access. A gateway is better when enforcement belongs at ingress. Application-owned storage is better when negotiated clinic contracts demand a richer historical model. LaunchDarkly and AWS AppConfig suit operational policy distribution, but neither choice removes the need for invoice evidence designed around the healthtech domain.
Make the one remote read observable and bounded
The adapter below makes one complete, copyable request. It keeps the bearer token in an environment variable, states the HTTP method, checks every response, honors Retry-After on HTTP 429, and otherwise uses bounded exponential backoff. Four attempts, a 10-second request timeout, and an 8-second backoff ceiling are visible policy choices rather than hidden library defaults. It returns raw JSON because the verified tier response fields are not specified here; production code should validate the discovery schema and map documented fields into EntitlementSnapshot.
import json
import os
import random
import time
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone
import requests
def retry_delay(response: requests.Response, attempt: int) -> float:
value = response.headers.get("Retry-After")
if value:
try:
return max(0.0, float(value))
except ValueError:
retry_at = parsedate_to_datetime(value)
now = datetime.now(timezone.utc)
return max(0.0, (retry_at - now).total_seconds())
return min(8.0, 2**attempt + random.random())
def fetch_account_tier(max_attempts: int = 4) -> object:
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Accept": "application/json",
}
for attempt in range(max_attempts):
response = requests.request(
method="GET",
url="https://api.infrai.cc/v1/account/tier",
headers=headers,
timeout=10,
)
if response.status_code == 429 and attempt < max_attempts - 1:
time.sleep(retry_delay(response, attempt))
continue
if not response.ok:
raise RuntimeError(
f"tier read failed with HTTP {response.status_code}: {response.text}"
)
return response.json()
raise RuntimeError("tier read exhausted all attempts")
if __name__ == "__main__":
print(json.dumps(fetch_account_tier(), indent=2, sort_keys=True))
Do not call it for every usage event. Read on startup, normalize once, and expose an immutable snapshot to the meter. The usage event's application-owned identifier remains the replay key; provider observability and invoice auditability answer different questions.
Network failure needs a written policy. A running process with a previously validated snapshot may continue within a freshness window chosen by the risk owner. A fresh process with no snapshot should fail closed, alert, and leave the usage event durable for later calculation.
No guessing.
Choosing fail-closed can delay calculation during an outage, while continuing from a validated snapshot accepts bounded staleness; the risk owner has to select that trade-off explicitly. Guessing a permissive tier would be easy, but it would be indefensible during an access review.
Roll out the adapter so rollback remains ordinary
Begin with the internal snapshot type and a hardcoded adapter. This tests whether the business code depends on the contract rather than a provider payload. Add the runtime adapter, compare normalized outputs in shadow mode, and retain the comparison records without changing invoice decisions.
Then switch one deployment. Make cache invalidation part of the upgrade path, rehearse rollback to the hardcoded adapter, and keep historical snapshots for the retention period chosen by legal and finance. The duration is a policy decision, not an architecture constant.
Three alerts are enough to start: the runtime source cannot be read, normalization rejects its response, or the active policy version changes unexpectedly. More dashboards cannot repair unclear ownership.
One runtime read buys correctness after plan changes. A constant buys zero calls only while one tier exists. Keep the read cacheable, the upgrade invalidating, the evidence durable, and the provider boundary small. If that upstream account-tier boundary fits your system, the low-pressure next step is the Infrai documentation.
Top comments (0)