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.
Where a key may live
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.
| Place | Verdict | Why |
|---|---|---|
| Server environment variable | Yes | The only place it is actually private. Injected at deploy, never committed. |
| A secrets manager or platform secret store | Yes | Better still: rotation and access are recorded rather than remembered. |
| Browser JavaScript, a mobile app, a game client | No | Shipped code is readable code. Anyone can extract it and spend your quota under your name. |
| A repository, even a private one | No | Clones, forks, CI logs and backups all outlive your intent to remove it. |
| A URL query string | No | Proxies, analytics and browser history all keep URLs. Send it in the header. |
The proxy that keeps it server-side
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.
// 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-Controlso 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.
One key per thing that can be revoked separately
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-prodis revocable with confidence;key 3is not.
Rotate without an outage
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.
# 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.If a key gets out
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.
- Create the replacementA new key first, so the fix is a deploy and not a scramble.
- Deploy itShip the new key everywhere the old one is read, and confirm traffic has moved in the per-key usage breakdown.
- Disable the old keyReversible, immediate, and it tells you at once whether anything was still using it.
- Check what it was used forThe usage breakdown shows the exposed key’s traffic. Unfamiliar volume is worth a support ticket.
- Delete itOnce a day has passed with nothing failing, delete the key and purge it from wherever it leaked.