Per-project API keys for LLMs — why one shared key fails
A single OpenAI key for the team is the default and the wrong default. What per-project keys actually buy you, what they cost to set up, and the failure modes a shared key creates.
- security
- governance
- best-practices
In our experience working with engineering teams, there's a moment — usually somewhere between the first prototype and the third deployed feature — when someone realizes that the OpenAI key they've been passing around in .env.local is now a production credential. By that point it's in a Vercel project, a GitHub Actions secret, two developer laptops, a shared Notion page, and at least one Discord screenshot. Rotating it means an afternoon of grep, redeploys, and "does this still work?" messages.
This post is about why that moment is avoidable, what the alternative looks like, and why "per-project API keys" is a feature you should think of as table-stakes rather than a nice-to-have.
What "per-project API keys" actually means
A per-project API key is a credential that's scoped to a single project, environment, or deployment unit, with its own rate limits, spending cap, and audit trail. Instead of one OPENAI_API_KEY shared everywhere, you have:
mg_live_<random>for the marketing site's chat widgetmg_live_<random>for the production Slack botmg_test_<random>for the staging environmentmg_live_<random>for the analytics pipeline
Each key answers a different request shape. Each key has a different budget. If one of them leaks, you revoke that one and only that one. The blast radius is small by construction.
This is the model AWS uses for IAM roles, the model Stripe uses for restricted keys, and the model basically every infrastructure provider has converged on. LLM access has been the exception, mostly because OpenAI launched with one key per organization and every team that started small never went back to fix it.
What goes wrong with a single shared key
The failures are predictable, and they happen to almost every team that hits real scale.
You can't tell which application caused the bill. We tested this on a real $2,847.16 invoice during one of our pre-launch audits: with one shared key across four features, attribution required reconstructing per-endpoint volume from log timestamps, which took most of an afternoon. With per-project keys, the same answer is one column on a dashboard. So when usage doubles, you don't know which feature got popular and you don't know which engineer to thank or which to ask about it.
You can't enforce limits per-feature. A bug in the new agent feature ends up in a retry loop. Each retry costs a dollar. The bug runs for an hour before someone notices. With one key, the only way to stop it is to revoke the key, which takes down every other feature using that key. With per-project keys, the agent's key has a $50/hour budget, the request returns HTTP 402 once the budget is hit, and nothing else is affected.
You can't audit who made changes. "Who turned on streaming for the report generator?" is a question that should have an answer in the audit log. With a shared key, every request is just "the team did it." With per-project keys plus a control plane, each key creation, rotation, or scope change is a discrete event with an actor.
Rotation is a deployment. When a key leaks (and they leak — into screenshots, into git history, into Slack DMs, into customer-facing error messages, into log files that get shipped to a third-party logging provider), the only way to rotate is to deploy a new key everywhere it's used. With per-project keys, you rotate the one that leaked, and the other twelve features keep running.
You can't give the right level of access to contractors. A contractor working on the Slack bot needs an OpenAI key. They don't need access to the marketing site's GPT-4o budget. With one key, you either give them the keys to the kingdom or you build a wrapper API yourself. Most teams build the wrapper API badly, on a Friday, three days before the contractor starts.
What "doing it right" looks like
A team running per-project keys at maturity has roughly this structure:
- One root organization owns the relationship with the gateway or provider.
- Within the organization, a project per deployable unit. "Production chat," "staging," "marketing site," "analytics ETL," "ML evals," etc. Each project is a logical container for keys, not a separate billing entity.
- Per-project budgets. Each project has a monthly cap. When the cap is hit, requests return 402. The cap is a hard ceiling, not a warning.
- Multiple keys per project, each with a label. "Production-primary," "production-canary," "Bob's laptop." The label survives rotation; the key cleartext doesn't.
- A dashboard showing spend, request count, and error rate per key. When a graph starts climbing, you know which feature owns it before you start debugging.
- Audit log. Every key creation, scope change, budget change, and revocation is a row with a timestamp and an actor.
- Rotation as a one-click action. "Rotate this key, give me a new one, leave the old one valid for 24 hours so deployment can roll over." This should not require a meeting.
This is not exotic infrastructure. This is what the providers build internally for themselves; we just don't usually expose it to customers.
The cost of getting it set up
This is the part teams underestimate. Per-project keys aren't free. Every key you create is one more place that needs to be in your secrets manager, in your CI environment, and in the development setup doc for new hires. If you have 14 projects and 3 environments each, that's 42 secrets to manage instead of 1.
This is solvable, and the solution has three parts:
-
A naming convention.
OPENAI_API_KEYbecomesMG_KEY_PRODUCTION_CHAT,MG_KEY_STAGING_CHAT, etc. Boring, predictable, greppable. -
A single source of truth. You should be able to ask one place "what keys does the production-chat project have, and which one should be rotated next?" — usually a dashboard, not a spreadsheet.
-
Tooling that mints keys directly into your secrets store. The path from "I need a key for this new project" to "the key is in Vercel/AWS Secrets Manager/Doppler" should be one command, not three handoffs. We expose this through an MCP server so coding agents can do it without humans in the loop, but a CLI works fine too.
If your team won't actually use per-project keys because the friction is too high, the feature has failed. The naming convention and the tooling are not optional.
The failure modes a shared key creates that nobody warns you about
A few that don't show up in the obvious "what if it leaks" framing:
The "we should add prompt logging" conversation gets harder. Some teams want to log full prompts and completions for debugging or compliance. With one key shared across consumer-facing chat and internal tooling, you can't turn on logging without also logging your customers' messages, which is a problem you don't want. Per-project keys let you turn on payload logging for the staging key only, where everything is synthetic.
You can't run two versions of a feature against each other. A/B testing model choices is hard when both branches are using the same key. With per-project keys, version A uses one key and version B uses another; you can compare cost-per-request directly from the dashboard without joining traces.
The first regulated customer kills the deal. When a healthcare or financial-services customer asks "how do you isolate our data from your other customers?" the answer "we have one key for everything" is a non-starter. You can't retrofit data isolation onto a shared-key architecture; you have to rebuild it.
Compromised laptops are silent. If a developer's laptop gets stolen and your shared key was on it, the attacker has full production access. You won't notice until they spike the bill. With per-project keys, the laptop has a single dev key, scoped to a single project, with a low budget. The attacker gets nothing useful.
How to migrate without a big project
If you're on a single shared key today and the idea of doing this migration feels like a quarter of work — it isn't, if you do it incrementally:
Week 1. Sign up for a gateway. Create one new key for one new feature. Don't touch existing features. You're learning the workflow on a low-stakes deployment.
Week 2. When the next existing feature hits a milestone (a redeploy, a refactor, anything that's already touching its config), swap its key for a new per-project one.
Week 3-N. Repeat. Each feature migration is small. After 6 weeks, your shared key has half its callers gone.
Eventually. Rotate the shared key. The features still using it break. You discover what was actually using it — which is information you didn't have before. Fix those. The shared key gets retired.
The whole point of this approach is to never have a "key migration project" on the roadmap. Migrations that are scheduled in advance get descoped under deadline pressure. Migrations that are tied to existing work happen as a side effect.
Why this is a gateway problem and not an "OpenAI feature request"
OpenAI does have project keys now. So does Anthropic. The catch: each provider's project keys only scope within that provider. If you're using both, you have two key systems to manage. If you add Gemini, three. The whole reason multi-model is interesting — different models for different tasks — is undercut by having to manage governance separately for each.
The gateway pattern fixes this by being the only thing you authenticate against. Your mg_live_* key works whether the underlying model is GPT, Claude, Gemini, or Llama. The audit log, budget, and rate limit are per-key, not per-provider. Adding a new provider doesn't mean adding a new governance surface.
This isn't a Synapse Garden specific argument. OpenRouter, Portkey, Helicone, and several others all give you this. The argument is "use one of them," not specifically "use ours."
What to take away
The shape of this problem doesn't change with scale; it just gets more painful. A team of three with one key is fine. A team of thirty with one key is paying for someone's hobby project somewhere and they don't know which someone. The cost of fixing it later is roughly proportional to the number of features the shared key touches.
If you're at the prototype stage, set this up before you ship anything to users. If you're past that, do the incremental migration. If you're at the "we have a leaked key on Twitter" stage, you already know what you should have done.
For implementation specifics, the authentication docs cover key creation, rotation, and revocation. To compare with adjacent products, the gateway comparison post covers how each gateway implements scoping.
Synapse Publication
Field notes, technical write-ups, and benchmarks from the team building Synapse Garden.
- Deep dive
Vercel AI Elements: 20+ React components for AI apps explained
A walk-through of every AI Elements component, what each one solves, and where rolling your own still wins. Practical patterns, real composition.
- How-to
Vercel AI SDK chatbot tutorial: useChat, streaming, real patterns
A working production-grade chatbot built on Vercel AI SDK v6. Streaming with useChat, tool calls, persistence, and the patterns that hold up after the demo.