GunSpec
Practice

Keys in production

A key is a bearer credential: whoever holds it is you. This is where one may live, how to keep it off the browser, and how to rotate one without an outage.

The test is simple: can somebody else read it? A key in anything you ship to a device is a key you have published, however it is obfuscated.

PlaceVerdictWhy
Server environment variableYesThe only place it is actually private. Injected at deploy, never committed.
A secrets manager or platform secret storeYesBetter still: rotation and access are recorded rather than remembered.
Browser JavaScript, a mobile app, a game clientNoShipped code is readable code. Anyone can extract it and spend your quota under your name.
A repository, even a private oneNoClones, forks, CI logs and backups all outlive your intent to remove it.
A URL query stringNoProxies, analytics and browser history all keep URLs. Send it in the header.

A browser that needs catalog data calls your server, and your server calls us. This is how gunspec.io itself works: the site holds no key, its proxy carries a server-side token and an allow-list of paths.

app/api/gunspec/route.ts
javascript
// A server route your app calls instead of calling GunSpec.// The key stays on this side; the browser never sees one.const ALLOWED = new Set(['/v1/firearms', '/v1/firearms/resolve', '/v1/calibers']) export async function GET(request) {  const url = new URL(request.url)  const path = url.searchParams.get('path') ?? ''   // An allow-list, not a pass-through. An open proxy is your key  // with extra steps: anyone who finds it spends your quota.  if (!ALLOWED.has(path)) return new Response('not allowed', { status: 403 })   const upstream = new URL(`https://api.gunspec.io${path}`)  upstream.search = url.searchParams.toString()  upstream.searchParams.delete('path')   const res = await fetch(upstream, {    headers: { 'X-API-Key': process.env.GUNSPEC_API_KEY },  })   // Pass the cache headers through: the answer is as cacheable for  // your visitors as it was for you.  return new Response(res.body, {    status: res.status,    headers: {      'Content-Type': 'application/json',      'Cache-Control': res.headers.get('Cache-Control') ?? 'public, max-age=300',    },  })}
  • Allow-list the paths you proxy. An open pass-through is your key with extra steps: anyone who finds it spends your quota.
  • Forward the upstream Cache-Control so your visitors get the same cacheability we gave you; see caching.
  • Rate-limit your own proxy per visitor. Our limits apply to your key as a whole, so one abusive visitor would otherwise spend everyone’s allowance.
  • Never echo the key back in a response, a log line or an error page.

Keys are free and usage is reported per key. A single shared key means an incident anywhere is an outage everywhere.

  • One per environment: production, staging, local. A leaked staging key then costs you a staging key.
  • One per service. When usage doubles, the per-key breakdown says which service changed.
  • One per contractor or integration you did not write, so ending the relationship is one click.
  • Name them for what they are. backend-prod is revocable with confidence; key 3 is not.

Two keys can be live at once, so rotation is a deploy rather than a cutover. Disable before you delete: disabling is reversible and deleting is not.

rotate.sh
bash
# Two keys live at once, so rotation is a deploy and not an outage.# 1. Create the new key in your account, alongside the old one.# 2. Ship it as the environment variable your service already reads.GUNSPEC_API_KEY=gsk_live_new... # 3. Watch traffic move: the usage breakdown reports per key.curl -sS -H "X-API-Key: $GUNSPEC_API_KEY" https://api.gunspec.io/v1/me/usage | jq '.data.perKey' # 4. Disable the old key. Disabled answers 401 KEY_DISABLED and can#    be switched back on; deletion cannot be undone, so disable#    first and delete once nothing has failed for a day.

Act on the key first and investigate afterwards. A disabled key answers 401 KEY_DISABLED immediately, and nothing is lost by disabling one you were unsure about. Tell us at support@gunspec.io if you think the exposure is ours; security has the disclosure route.

  1. Create the replacementA new key first, so the fix is a deploy and not a scramble.
  2. Deploy itShip the new key everywhere the old one is read, and confirm traffic has moved in the per-key usage breakdown.
  3. Disable the old keyReversible, immediate, and it tells you at once whether anything was still using it.
  4. Check what it was used forThe usage breakdown shows the exposed key’s traffic. Unfamiliar volume is worth a support ticket.
  5. Delete itOnce a day has passed with nothing failing, delete the key and purge it from wherever it leaked.