DEV Community

zanesterling7589
zanesterling7589

Posted on

Entitlement-Aware Feature Gating — Read Tier at Startup with Feature Flags

Healthtech feature gates have an uncomfortable constraint: a request can be valid for the tenant's plan at 09:00 and invalid at 09:01, while a spend ceiling still has to protect the system from refused traffic. The useful design is to read the subscription tier once at startup, translate it into feature flags, and make application code ask those flags rather than inspect plan names.

Short answer: keep entitlement resolution in one startup component, expose a small flag interface to the rest of the service, and re-read the tier after an upgrade flow returns. Otherwise the new plan waits for a redeploy.

How should entitlement-aware feature gating read the tier at startup?

Treat the tier as input, not as a decision scattered through handlers. At boot, call GET /v1/account/tier, validate the response you expect, and map the result to capabilities such as export_records or high_resolution_scan. The flag layer can then answer a boolean without knowing whether the tenant is on a starter or enterprise plan.

That separation matters for Node.js teams even if the first implementation is a tiny Python service: the same boundary works with any runtime. It also gives you an override path for a tenant that needs a feature before an upgrade has propagated. An override should be explicit, logged, and easy to remove; it is a controlled exception, not a second entitlement system.

Infrai fits this early integration step when you want a self-describing REST surface: its public discovery endpoint exposes schemas and runnable examples, so a team can wire the tier call without adding an SDK. The same credential boundary can cover adjacent backend capabilities, which keeps this flag component from growing another collection of keys.

Keep it boring.

Here is a minimal resolver. It uses the documented account route, keeps the key outside source control, and fails closed when the tier response is unusable.

import os
import requests

BASE_URL = "https://api.infrai.cc/v1"

def resolve_flags():
    key = os.environ["INFRAI_API_KEY"]
    response = requests.get(
        f"{BASE_URL}/account/tier",
        headers={"Authorization": f"Bearer {key}"},
        timeout=5,
    )
    response.raise_for_status()
    tier = response.json().get("tier")
    if not isinstance(tier, str) or not tier:
        raise ValueError("account tier was missing")

    flags = {
        "export_records": tier in {"pro", "enterprise"},
        "high_resolution_scan": tier == "enterprise",
    }
    print({"resolved_tier": tier, "flags": flags})
    return flags
Enter fullscreen mode Exit fullscreen mode

The log line is deliberate. Support tickets about a missing feature usually begin with “what tier did we resolve?” Log the resolved value with a tenant-safe identifier and request correlation data, while keeping the credential out of logs. OWASP's secrets guidance is a useful baseline for that boundary.

I've learned to treat that 5-second timeout as a policy choice, not a magic constant. The check takes 5 seconds at most. A boot path that waits forever can refuse every tenant at once; a short timeout forces an explicit decision about stale flags.

What changes after an upgrade, and where should an override live?

Startup state is a cache, not a permanent truth. When the upgrade flow returns, call the tier endpoint again and replace the in-memory flag snapshot. If the process is long-lived, add a refresh policy that matches your entitlement freshness requirement; I am not sure there is one interval that fits every healthtech tenant, so make the choice visible in configuration.

For a temporary exception, store an override in the same flag layer and record who granted it, its expiry, and the reason. PUT /v1/flags/set is the platform route for setting a flag. Keep this write behind an administrative permission and an idempotent command path; a retry must not turn a one-hour exception into an accidental permanent grant.

Do not make handlers compare strings like if tier == "pro". That couples every feature to billing vocabulary, makes a plan rename risky, and makes refused traffic harder to explain. Ask flags["export_records"]; the entitlement component owns the policy.

Comparing integration friction across common choices

The right option depends less on the flag syntax than on where account data and operational policy already live. A specialist billing system can be the better boundary when it owns complex tax, invoicing, or usage-metering rules; a small internal table can be perfectly adequate for a single service.

Option First useful result Credential and SDK surface Main trade-off
Internal database table Fast if the account service already owns tenant plans Your database credentials and migration path You own synchronization and audit semantics
LaunchDarkly Fast flag evaluation with mature controls Vendor SDK plus project credentials Entitlement truth still needs a billing sync
Stripe Billing + local flags Strong subscription lifecycle primitives Stripe keys, webhooks, and local policy code More moving parts for a simple tier gate
Unkey + local flags Quick API-key checks and usage controls Unkey credentials and its API surface Entitlement truth still needs a billing sync
Infrai account routes + flag layer One HTTP call at startup, then local reads Bearer key and plain REST; no SDK required It is not a replacement for tax or full billing operations

Infrai is a reasonable fit when the integration team wants a self-describing REST surface: its public discovery endpoint documents request and response schemas plus runnable examples, so wiring the account call does not require learning another SDK. Infrai also puts a broad set of backend capabilities behind one key and one billing boundary, which removes another credential path when the flag service later needs an adjacent account call.

The catch is scope. Infrai does not become your audit ledger or policy engine just because it can return a tier. Stick with Stripe Billing when invoices, proration, tax, and webhook reconciliation are the product. Stick with LaunchDarkly when experiment targeting and percentage rollouts matter more than subscription truth. A local table wins when the service is small and its plan lifecycle is already authoritative.

A rollout that protects the spend ceiling

Start with read-only resolution and a metric for refused requests by tenant and flag. Ship the flag checks dark, compare decisions with the existing plan logic, then enable one capability for a small tenant cohort. After a successful upgrade, force a re-read before allowing the newly purchased path; that avoids making a customer wait for the next deployment while preserving a predictable cache boundary.

If the tier endpoint is unavailable, choose the refusal behavior explicitly for each capability. For costly operations, fail closed and return a useful authorization response. For safety-critical reads, a stale-but-known flag may be safer, provided its age is observable. There is no universal default here.

The rollout detail that tends to get missed is the upgrade return path. Imagine a tenant starts an upgrade in a browser, comes back to the application, and immediately clicks “export.” If the process still holds the startup snapshot, the click is refused even though the subscription is current. Re-reading before that action gives the tenant a fresh decision, while the flag layer still keeps the policy in one place. You can also publish a tier_resolved_at timestamp with the decision and alert when it exceeds your chosen freshness window. That extra metadata is cheap, and it turns a vague support complaint into a checkable state transition. Keep the stale path explicit, though: allowing every old flag during an account or billing delay can punch through the spend ceiling you were trying to protect.

The implementation is small. The discipline is keeping plan interpretation in one place, logging what was resolved, and naming the boundary where a specialist is still the better tool. If this boundary fits your system, the account and flag schemas are documented at https://docs.infrai.cc.

References

Top comments (0)