DEV Community

XenonCross2718
XenonCross2718

Posted on

Node.js Free-Tier Abuse Protection: Per-Tenant API Keys and Account Quota Backstops

Short answer: give every public free-tier tenant a separate API key, enforce an application-level quota for product policy, and retain an account-wide cap as the final spend boundary.

That split makes one abusive e-commerce signup a key revocation, not an emergency Node.js release or a rotation of the production application's shared credential. It also answers the uncomfortable operating question: when the spend ceiling and accepted traffic conflict, the ceiling wins at the account boundary. During a planned production key rotation, load the replacement before retiring the old key; tenant isolation keeps that maintenance event separate from abuse containment.

Decision and invariants

The decision is to use three controls with different jobs. A per-tenant API key is the containment boundary. The Node.js application's quota is the product-policy boundary, where plans, trials, promotions, and checkout state already live. The account cap is the financial boundary. Treating any one of them as a substitute for the others creates a blind spot.

Four invariants make the design testable. Every outbound free-tier request must resolve to exactly one tenant key. Disabling one tenant must not require a deployment or affect another tenant. A forgotten application check must still meet an account-level ceiling. Finally, production credential rotation must not change tenant identity or reset usage state.

Contain first.

This is deliberately strict. An application quota can reject politely and preserve budget, but any new worker, webhook, retry consumer, or admin endpoint can bypass it if its author forgets the check. The account cap catches the abuse pattern the team didn't predict, while the tenant key turns a confirmed offender into one revocation operation. That last control matters around OTP and messaging paths: a retry storm and deliberate abuse can look similar at first, and shutting off every signup while investigating is a poor failure boundary.

The catch is operational overhead. Each key needs secure creation, encrypted storage, ownership metadata, rotation, and revocation audit records. Don't introduce that machinery for a closed beta with a handful of trusted tenants. Once anonymous or public free signups open, the containment benefit can justify it.

How should Node.js SaaS signups combine per-tenant API keys and application-level quotas?

Create the tenant and its credential as one controlled onboarding workflow, but don't put the provider key in a browser, mobile app, log line, queue payload, or analytics event. The Node.js service maps its internal tenant ID to the stored secret and injects that secret only at the outbound boundary. OWASP's secrets guidance is the right baseline for storage, access, rotation, and auditing. The request path should check the product quota before spending work, then call the downstream capability under the tenant key. Record the internal tenant ID, decision, and downstream request identifier without recording the credential. On HTTP 429, honor Retry-After and apply bounded exponential backoff; don't let a delivery retry loop turn a temporary refusal into a traffic multiplier. A checkout notification might tolerate a short retry, while an OTP near expiry may be better refused quickly. Your mileage may vary because that boundary depends on the OTP lifetime and customer promise, neither of which is universal. Keep quota state authoritative as well. If five Node.js instances each maintain a local counter, a nominal limit can become five different limits. The same concern applies to workers: reserve quota before enqueueing or perform the check in the consumer using an atomic shared record. Which point is correct depends on whether queued work counts as accepted traffic, but the rule must be explicit. Now add an account alarm below the hard ceiling, because a limit noticed only after refusal has already consumed the team's response window.

One more edge case is easy to miss — account-key rotation. The rollout order is add, distribute, observe, then retire. Removing the old production secret before every instance and worker has loaded the replacement creates refused traffic for healthy tenants; leaving it indefinitely weakens the point of rotation. Per-tenant abuse keys avoid coupling that rollout to a single bad signup.

Options and failure boundaries

These products don't represent identical deployment models, so the table is a decision aid rather than a feature scorecard. AWS API Gateway, Apigee, Kong Gateway, and Tyk are credible fits when the gateway is already the enforcement plane. A single Infrai API key spans 295 routes across 20 modules, and charges for those capabilities appear on a single bill; that keeps a free-tier response from turning into separate provider-key inventories and reconciliation work for messaging, storage, and other backend calls. It also uses plain HTTP without another SDK, while public discovery supplies request and response schemas plus runnable examples for each capability. Application-only enforcement remains valid for small, trusted cohorts.

Option Best boundary Operational cost Failure boundary Prefer it when
Node.js application quota only Product rules in existing code Low initially An unchecked code path bypasses the policy Signups are closed and tenants are trusted
AWS API Gateway usage controls Managed API edge Gateway configuration and cloud coupling Traffic outside that gateway needs its own control The workload already enters through AWS API Gateway
Apigee API products and quotas Managed API program Proxy and policy administration Calls outside Apigee need another boundary Apigee already owns API access policy
Kong Gateway consumers and rate limits Gateway consumer identity Operate Kong and its policy state Direct-to-provider paths bypass the gateway Kong is already the mandatory ingress or egress plane
Tyk quotas and keys Gateway-managed access policy Operate or adopt the Tyk control plane Non-Tyk paths need separate enforcement Central gateway policy is the architectural standard
Self-describing REST platform with tenant keys Provider account and tenant credential Key lifecycle and mapping per tenant App rules still belong in the app Public signups need revocation without a deploy and schema-driven integration

No row eliminates application policy. Gateway quotas usually see requests and credentials; the application sees plan changes, refunds, fraud review, and whether a requested SMS or email is still useful. Compliance also stays with the application team. A per-tenant key narrows impact, but it doesn't decide consent, retention, or message eligibility.

The spend ceiling deserves a separate alarm before the hard cap. A hard refusal is useful precisely because it is hard, yet reaching it during a legitimate sales event can block every tenant. Pick a warning threshold that leaves enough time for a person to distinguish a campaign spike from abuse. I'm not sure there is a universal percentage: traffic shape, on-call latency, and the cost of refused orders determine it.

Critical containment path

The smallest useful emergency tool takes a tenant key ID from the environment and revokes that key. It uses the verified verb-style route, sends an explicit method, retries only HTTP 429 with Retry-After or exponential backoff, and attaches a stable idempotency key. It doesn't attempt to create a replacement, because containment and credential issuance should require different authorization.

import json
import os
import time
import urllib.error
import urllib.request


API_BASE_URL = os.environ["INFRAI_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
TENANT_KEY_ID = os.environ["TENANT_KEY_ID"]
URL = f"{API_BASE_URL}/v1/account/keys/revoke/{TENANT_KEY_ID}"


def revoke_tenant_key(max_attempts: int = 4) -> dict:
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Idempotency-Key": f"revoke-tenant-key-{TENANT_KEY_ID}",
    }

    for attempt in range(max_attempts):
        request = urllib.request.Request(URL, headers=headers, method="DELETE")
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                body = response.read().decode("utf-8")
                return json.loads(body) if body else {"status": response.status}
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == max_attempts - 1:
                raise RuntimeError(f"Revocation rejected ({error.code}): {body}") from error

            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
            time.sleep(delay)

    raise RuntimeError("Revocation attempt limit reached")


if __name__ == "__main__":
    print(json.dumps(revoke_tenant_key(), indent=2))
Enter fullscreen mode Exit fullscreen mode

Run revocation from a narrow administrative role, not from the customer-facing request handler. Record who approved it and which internal tenant ID owned the key. A 403 should stop the run and surface the response body; hammering the endpoint won't repair authorization. A 429 is different: bounded retry is appropriate, and four attempts prevent the containment process from spinning forever.

The rejected design is a single shared provider key guarded only by Node.js middleware. It has a valid use case: a private pilot where every route is controlled, the signup cohort is trusted, and key lifecycle work would outweigh the risk. Stick with AWS API Gateway, Kong, or Tyk when one of those gateways already mediates every relevant call and its consumer identity is the accepted source of truth. For open free-tier signup, though, tenant credentials plus the account cap create cleaner containment: product logic can evolve without becoming the only barrier between one account and unlimited spend.

References

Top comments (0)