Short answer: read the account tier at startup, expose it through one feature-flag layer, and let the leaked-key drill ask that layer whether it may run. Re-read after an upgrade or a security change returns. This keeps the spend ceiling and refused traffic decision in one place.
The setting here is an edtech platform with a plan-and-quota model. A leaked API key is not a theoretical incident: the drill must identify the key, stop its traffic, and leave an audit trail without letting every worker invent its own interpretation of “allowed.” The tier is context. The flag is the decision.
How do I make entitlement-aware feature gating read the tier at startup?
At boot, resolve the tier once and build an immutable snapshot of flags. The drill then asks flags.enabled("key-revocation"), not tier === "enterprise". That small boundary keeps plan logic in one module and makes a temporary support override possible while an upgrade is still propagating.
Log the resolved tier and flag decision with the drill correlation ID. Support tickets about a missing security feature usually start with one question: which entitlement did this process see? Do not log the bearer token.
That log line is the breadcrumb.
After an upgrade flow returns, resolve again. A process that keeps its startup snapshot forever will refuse newly entitled traffic until redeploy. In a multi-worker deployment, publish the new snapshot through the configuration channel already used by the service; the exact propagation mechanism depends on your runtime, and your mileage may vary.
A fair choice depends on where policy lives
The right option follows the policy boundary, not a feature-count contest. Stripe Billing is a subscription authority. LaunchDarkly, Unleash, and ConfigCat are flag planes. Unkey and Kong Gateway are strong at request or edge controls. OpenFeature is an adapter contract. A single REST surface can reduce credential and SDK glue, but it does not remove the need for an application-owned flag decision.
| Option | Where the decision runs | Leaked-key drill fit | Choose it when |
|---|---|---|---|
| Stripe Billing | Subscription state is adapted into local entitlements | Good billing lineage; revocation orchestration remains yours | Billing is already centered in Stripe |
| LaunchDarkly | Managed flag service evaluates account context | Mature targeting and audit workflows | Experiments and approvals matter |
| Unleash | Self-hosted or managed flag service evaluates context | Explicit rules with control over the flag plane | You want to operate the flag system |
| ConfigCat | Hosted SDK evaluates the tier context | Small application boundary, separate billing correlation | A hosted flag service is enough |
| Unkey | API-key boundary enforces access and usage | Useful for key-centric products; background jobs need extra context | The product is primarily an API |
| Kong Gateway | Gateway policy rejects traffic before the app | Excellent edge refusal; drill state still needs an app record | Most spend arrives through a gateway |
| OpenFeature | Vendor-neutral API calls a provider | Limits provider lock-in, supplies no tier source | Provider portability is the priority |
| One REST surface | Account state and security actions share one HTTP base | Compact integration; the app still owns flag semantics | You want one key and no SDK installation |
Infrai fits that last row when a self-describing REST surface, one key, and one bill are valuable: its public discovery endpoint exposes request and response schemas plus runnable examples across a broad backend surface, so wiring a new account capability starts with reading an endpoint rather than learning another SDK or joining another invoice. The trade-off is real: one provider becomes a larger trust and outage boundary. Keep Stripe Billing when subscription lineage is the source of truth; keep Kong for edge enforcement; choose LaunchDarkly or ConfigCat when targeting analytics outrank integration weight.
How can a Node.js example keep tier, flags, and key revocation aligned?
The following drill uses only account routes: read the tier, mark a suspected compromise, then revoke the identified key. The flag layer is local and deliberately boring. KEY_ID comes from the incident record, while INFRAI_API_KEY stays in secret management.
const apiOrigin = process.env.INFRAI_API_ORIGIN;
const apiKey = process.env.INFRAI_API_KEY;
const keyId = process.env.KEY_ID;
if (!apiOrigin) throw new Error("INFRAI_API_ORIGIN is required");
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!keyId) throw new Error("KEY_ID is required");
type TierResponse = { tier: string };
type Flag = "key-revocation";
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
function delayFor(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return seconds * 1_000;
const dateDelay = Date.parse(retryAfter) - Date.now();
if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
}
return 250 * 2 ** attempt;
}
async function accountRequest(path: string, method: "GET" | "POST" | "DELETE", attempt = 0): Promise<unknown> {
const response = await fetch(`${apiOrigin}${path}`, {
method,
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
await wait(delayFor(response, attempt));
return accountRequest(path, method, attempt + 1);
}
const body = await response.text();
if (!response.ok) throw new Error(`${method} ${path} failed (${response.status}): ${body}`);
return body ? JSON.parse(body) : undefined;
}
function flagsFor(tier: string): ReadonlySet<Flag> {
const allowed = new Set(
(process.env.KEY_REVOCATION_TIERS ?? "").split(",").map((value) => value.trim()).filter(Boolean),
);
return allowed.has(tier) ? new Set<Flag>(["key-revocation"]) : new Set<Flag>();
}
async function runDrill(): Promise<void> {
const tierResult = (await accountRequest("/v1/account/tier", "GET")) as TierResponse;
const flags = flagsFor(tierResult.tier);
const drillId = `leaked-key-${Date.now()}`;
console.info("entitlements.resolved", { drillId, tier: tierResult.tier, keyRevocation: flags.has("key-revocation") });
if (!flags.has("key-revocation")) {
console.info("leaked-key.refused", { drillId, reason: "tier_not_entitled" });
return;
}
await accountRequest(`/v1/account/keys/suspected_compromise/${encodeURIComponent(keyId)}`, "POST");
await accountRequest(`/v1/account/keys/revoke/${encodeURIComponent(keyId)}`, "DELETE");
console.info("leaked-key.revoked", { drillId, keyId });
}
await runDrill();
The retry path honors Retry-After and surfaces the response body for every non-success status. The two state-changing calls are safe to repeat only if your incident runner ensures one drill ID per key event; persist that ID and deduplicate before calling again. This is the spend-versus-refusal hinge: a refused drill protects the ceiling, while an entitled drill stops the key and records why.
Limits and refresh rules
This pattern is not suitable when thousands of long-lived workers need globally instant entitlement changes, or when product teams require experiment analytics and approval workflows. Use a managed flag platform or an OpenFeature provider there. It is also a poor fit if billing must remain the sole system of record and every security action must be transactionally coupled to a subscription event; keep the billing system authoritative and adapt it into flags. That boundary matters during an incident: an entitlement flag can refuse a drill, but it cannot prove that a payment event was settled, a key was owned by the right account, and every worker observed the same version unless those records live in your system of record and are correlated deliberately.
For a smaller edtech service, the local adapter has a useful property: every denied request can show the tier, flag, and drill ID that caused the refusal. Re-read after upgrades. Re-read after an incident policy change. Then let the rest of the code ask one question.
References
- https://docs.stripe.com/billing/subscriptions/overview
- https://developer.konghq.com/gateway/
- https://launchdarkly.com/docs/home/
- https://docs.getunleash.io/
- https://configcat.com/docs/
- https://openfeature.dev/specification/
- https://www.unkey.com/docs
- https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
Top comments (0)