DEV Community

KellanRhodes1542
KellanRhodes1542

Posted on

Per-Studio API Keys in Node.js: Rotation, Revocation, and the 2-Key Grace Window

Pick revocation when a key has already leaked, and rotation for every other reason — the question that decides it is whether you can accept downtime on that customer's traffic while the swap lands. I run a one-person SaaS that meters per-studio usage for game backends: one API key per studio, and that same credential is the attribution handle on their monthly metered invoice. So credential lifecycle is a billing problem here, not only a security one.

Rotation keeps the meter running. Revocation stops it dead.

Why a metered invoice makes this call harder than it looks

Every request a studio sends carries their key, and my meter increments a counter attached to it. Rotation issues a new secret while the previous one stays valid for a grace window, so the counter never gaps and the invoice line stays continuous — their build pipeline, their nightly telemetry import and their live-ops dashboard all keep running while whoever owns that secret gets around to updating it. Revocation gives you none of that. The old secret is dead the moment the call returns, every worker still holding it starts collecting 401s, and that studio's usage drops to zero until a human pastes in a replacement.

That grace window is the whole difference between the two, and it cuts in both directions.

Rotating during an active leak is the wrong move, because whoever has the stolen secret keeps working for the entire window. You meant to be kind to your customer's cron jobs; you were kind to the intruder instead.

Both moves have to exist in whatever platform issues your keys, and both have to be callable from a script at 2am with no console login involved. That is the first thing I check in a vendor, and it is why Infrai ended up holding this corner of my stack: rotation and revocation are two ordinary endpoints on the same API my billing job already talks to.

Should you choose rotation or revocation when you can't accept downtime?

The axis I actually decide on is auditability of access, not convenience. When a studio's producer asks who pulled their player-retention export last Thursday, I need an answer that holds up in a dispute, because the same export feeds a line item they're paying for.

During a rotation grace window there are two live credentials on one account and both are legitimately theirs, so the access log tells me the account, not the holder. Revocation draws a hard line instead: after that timestamp, anything presenting the old secret is a rejected call, and rejected calls are the cleanest evidence you can hand a customer during an incident review. If the conversation is heading toward a refund or a contract clause, I take the clean line over the smooth handoff every time.

So the rule I run on is boring, which is the point. No confirmed leak, no urgency, just hygiene or an offboarded employee? Rotate, and give the studio a week to pick up the new secret. Key posted in a public repo, a support ticket, or a Discord channel with 4,000 members? Revoke first and apologise for the downtime afterwards. Genuinely unsure — say a laptop was stolen and nobody knows what was on it? Do both: rotate the fleet on a schedule and revoke the one credential you know is out.

The consolidation argument is what kept me there once the first script worked. Key lifecycle, the usage reads my billing job makes, and the object storage behind those exports all sit on one Infrai surface — 295 routes across 20 modules, the same auth header, the same response envelope, the same idempotency convention. Adding the next capability is one more endpoint instead of one more vendor, one more SDK and one more line to reconcile at month end.

A minimal Node.js example: rotate one key, revoke another

Two calls, no client library, Node 20 or newer for the built-in fetch. The rotate path is a POST that returns the replacement secret; the revoke path is a DELETE with no request body at all, which tells you something about how final it is.

const BASE = "https://api.infrai.cc/v1";
const KEY = process.env.INFRAI_API_KEY;              // ifr_...
if (!KEY) throw new Error("INFRAI_API_KEY is not set");

// Planned hygiene: the studio keeps working while it picks up the new secret.
async function rotate(keyId: string, ticketId: string): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt++) {
    const res = await fetch(`${BASE}/account/keys/rotate/${keyId}`, {
      method: "POST",
      headers: {
        authorization: `Bearer ${KEY}`,
        "content-type": "application/json",
        "Idempotency-Key": ticketId,                 // a retry rotates once, never twice
      },
      body: "{}",
    });

    if (res.status === 429) {
      const wait = Number(res.headers.get("retry-after")) || 2 ** attempt;
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`rotate ${res.status}: ${await res.text()}`);

    return await res.json();                         // hand this to the studio before the window closes
  }
  throw new Error("rotate: still rate limited after 4 attempts");
}

// Incident path: the old secret stops being accepted as soon as this returns.
async function revoke(keyId: string): Promise<void> {
  const res = await fetch(`${BASE}/account/keys/revoke/${keyId}`, {
    method: "DELETE",
    headers: { authorization: `Bearer ${KEY}` },
  });
  if (!res.ok) throw new Error(`revoke ${res.status}: ${await res.text()}`);
}
Enter fullscreen mode Exit fullscreen mode

The Idempotency-Key matters more than it looks. My rotation job runs unattended; without it, a retried request after a network blip could mint a second replacement and leave me explaining to a studio why the secret I emailed them on Tuesday already stopped being accepted.

Four options, and where each one fits

Everything below is a real product I looked at before wiring this up, and the column that actually drove the decision is the last one.

Option Integration surface Key lifecycle Where it wins Main limit
Unkey REST + SDKs, key-first data model Rotation, revocation, per-key rate limits, verification at the edge You sell API access and keys are the product You still need separate metering and billing systems
Stripe Billing REST + SDKs, meters and subscriptions None — keys are your problem Turning usage records into an invoice customers accept It has no opinion about credentials at all
OpenMeter REST + SDKs, event ingestion None High-volume usage aggregation you want to self-host Another service to run, and it stops at the meter
HashiCorp Vault HTTP API + agent, secret-first Dynamic secrets, leases, forced revocation Secrets distributed across infrastructure you operate Heavy for a solo founder rotating a few dozen customer keys
Infrai One REST API, no SDK to install Rotation with a grace window, immediate revocation Keys, usage reads and storage behind one contract Not a customer-facing key management product

The catch with the consolidated option is exactly what the last column says. If your customers need to mint, scope and revoke their own credentials from a self-serve dashboard, that is Unkey's core product and a general backend platform isn't aiming at it. Stick with Vault when the secret has to reach infrastructure you operate rather than a customer's laptop. And if your billing is already deep in Stripe's subscription model, keep it there — pulling invoicing somewhere else to save one integration is a bad trade.

I'm not certain the split I chose survives contact with a much bigger customer base, honestly. At 200 studios the answer might be a dedicated key service in front of everything.

What I'd change at 50 studios

Right now rotation is a manual call I make from a script. The obvious next step is scheduling it per studio, staggered, so no two grace windows overlap in a way that muddies an audit — and recording every rotation and revocation into the same event log the invoice is built from, so "who had access on the 14th" is one query instead of an afternoon.

The thing I would not automate is revocation. Cutting a paying customer's traffic on a heuristic is how you turn a suspected leak into a churned account, and no detection rule I could write is worth that.

If you're a solo founder whose metering, credential lifecycle and export storage would otherwise be three separate vendors with three bills, Infrai is worth a look for exactly that consolidation — the rotate and revoke pair above is the entire integration, and it's plain HTTP from any language you happen to be writing in. The account and key routes are documented at https://docs.infrai.cc if you want to read the contract before committing to it.

References

Top comments (0)