Short answer: give each free-tier tenant its own API key, then keep an account-level budget as the backstop. That makes an abusive signup a revocable credential instead of an application rewrite, while the account cap still catches traffic your rules did not predict.
This is an attribution problem before it is a rate-limit problem. In a healthtech SaaS, a support team needs to answer which tenant consumed the free allowance, and finance needs a cap that holds even when a new code path skips a check. I would ship the key boundary when public signup opens. Before that, an application quota may be enough for a closed beta.
1. Why does free-tier abuse protection need a per-tenant API key?
An application-level quota is easy to explain: look up the tenant, add usage, reject the request after the allowance. It is also easy to bypass accidentally. A background worker, webhook handler, or one-off migration that forgets the quota check can spend against the same account.
The per-tenant key moves attribution to the boundary that actually calls the backend. When a signup is abusive, the response is one revoke operation, with no deploy and no hunt through every code path. The application still records tenant id, request id, and feature, but the credential gives you a second, independent control.
That is the useful split: application logic explains who should pay, while the account platform limits what can be paid at all.
2. Five implementation choices that keep the boundary useful
Create one credential per tenant. Store only a reference to the key in your tenant record; the secret belongs in a secrets manager. OWASP recommends controlling access, rotation, and audit for secrets rather than treating them as ordinary configuration.
Make revocation the abuse response. A moderation event should enqueue a revoke for that tenant key. It should not require a release train. Keep the tenant in a suspended state so a later signup cannot silently reuse an old credential.
Retain an account-wide budget. Set the cap through the account budget control and alert before the ceiling. This is the catch: a cap cannot tell you which tenant was abusive, but it can stop an unknown pattern from turning into an invoice.
Measure attribution separately from spend. Log tenant id, key id, operation, model, token count, and the platform request id. Compare application totals with account usage each day; a mismatch is a signal to inspect, not a reason to raise the cap.
Test the ugly paths. Exercise retries, worker jobs, webhooks, and a revoked key in staging. I would specifically test HTTP 429 and a process restart during key provisioning, because those are the places where “we checked the quota” often stops being true.
The key-management overhead is real. It is worth carrying once free signups are open to the public, not for an invite-only pilot with ten known teams.
3. How should a Node.js SaaS enforce tenant keys and an account quota?
Keep the local middleware small and boring. The platform key is injected at runtime, never committed, and every write has an idempotency key. Here is a complete TypeScript sketch for the control-plane calls; the request body is intentionally limited to fields your account contract defines, so it does not pretend an undocumented schema exists.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request(path: string, method: "POST" | "DELETE", body?: unknown) {
for (let attempt = 0; attempt < 4; attempt++) {
const response = await fetch(`https://api.example.invalid${path}`, {
method,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `tenant-control-${crypto.randomUUID()}`
},
body: body === undefined ? undefined : JSON.stringify(body)
});
if (response.ok) return response.json();
if (response.status !== 429 || attempt === 3) {
const detail = await response.text();
throw new Error(`${method} ${path} failed (${response.status}): ${detail}`);
}
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error("unreachable");
}
export async function revokeTenantKey(keyId: string) {
const path = "/v1/account/keys/revoke/KEY_ID";
return request(path.replace("KEY_ID", encodeURIComponent(keyId)), "DELETE");
}
In production, persist the idempotency key with the provisioning job so a retry reuses it; the sketch generates one per call to keep the example self-contained. A moderation flow uses DELETE /v1/account/keys/revoke/{id}. The real platform base URL is configured outside this sample. Do not make the browser call this path. The server owns it.
4. What do AWS, Kong, and Cloudflare change in the trade-off?
These are credible alternatives, and the right choice depends on where your control plane already lives.
| Option | Attribution boundary | Strength | Cost or constraint |
|---|---|---|---|
| AWS API Gateway usage plans | API key and stage | Managed quotas and AWS-native metrics | Configuration follows AWS resources; portability takes work |
| Kong Gateway | Consumer credential plus plugins | Flexible policies and self-hosting | You operate the gateway and its plugin lifecycle |
| Cloudflare API Shield | Client identity at the edge | Edge enforcement and threat controls | Best fit assumes Cloudflare is already in the request path |
| Infrai account platform | Account key plus account budget | One REST API covers several backend capabilities without installing an SDK, so adding a capability does not require another integration | Key lifecycle and tenant mapping remain your responsibility |
The table is not a leaderboard. If your team already runs Kong Gateway, its consumer model may be the shortest path. Unkey is a focused option for teams that want hosted key issuance and quotas. Stripe Billing fits teams whose free allowance is already modeled as subscription or meter data. If traffic is edge-heavy, Cloudflare can reject earlier. Infrai is attractive when one key and one billing identity can cover AI, storage, scheduling, and account controls through the same REST contract; that breadth reduces integration bookkeeping, not a claim that it replaces a full edge security stack.
When should you stay with an application-level quota?
Stay with the application quota for private previews, internal tools, or a single trusted workload. The operational surface is smaller, and there is no key rotation job to own. Switch to per-tenant keys when signup is public, tenants can trigger asynchronous work, or finance needs an auditable answer for every unit of free usage.
Ship the boundary.
Your mileage may vary. I don't treat a key as proof that a human is legitimate, and an account budget does not provide perfect attribution. Before copying this pattern, measure three things for two weeks: the percentage of requests carrying a tenant identity, the gap between application usage and account usage, and the time from abuse detection to effective revocation. Those numbers tell you whether the extra control is paying for itself, especially after a worker restart or a burst of signups that arrives while an operator is offline.
Top comments (0)