A side project I was building needed five API keys before it would do anything at all. One for the language model, one for sending notifications, two for posting to social platforms, one for the database. Five secrets, minimum, just to find out whether the prototype was any good.
The normal way to handle that is a config file. Open the editor, find .env.local, paste the key, save, restart the dev server, hope it worked. It is not broken. It is just slow, and it requires being a coder. Swapping from Sonnet to Haiku for one test costs the same six steps as setting it up in the first place.
Which is a strange thing to have standing between me and finding out whether an idea works.
So I did not put any of them in a config file. I built a settings page into the app instead, and whoever is using it, me while testing or anyone I send the link to, pastes their own keys into the running app.
Every key the app needs has a field. Type it, save it, the app uses it on the next call. Same code, configured while it is running rather than before it starts.
The loop went from edit, save, restart, test to paste, click, test. Changing models became a dropdown. Testing against a different key became a paste. And the prototype turned out to be shareable as a side effect: send someone the URL, they put in their own keys, and it runs against their accounts.
Whose key, and where it is used
Whether any of this is a reasonable thing to do comes down to two questions, and most of the argument about it only asks the first.
Whose key it is. If it belongs to the user, they pay the bill and they control the account, this is increasingly ordinary. Cursor and the OpenRouter playgrounds work this way. If it is my app’s own production key, the one that bills my account every time anyone makes a request, then never. That belongs in environment variables locked down on a server, where the browser cannot see it. The test I use is simple: whose pocket does this API call come out of?
Where it gets used from. The simplest build has the browser call the AI service directly, which means the key is visible in the browser’s network log on every call. There is no hiding it. That is fine for a personal tool where it is your key, in your browser, against your account. The alternative is routing through my own backend: the key still lives in the browser, but it is not sitting in network responses. More to build, a much better security profile, and the one I take when I care.
The architecture matters more than the pattern.
- If the key lives in the browser, it's exposed. Anything running on your page can read your browser's local storage — your own code, a chat widget, a compromised library. A single bad script leaks the key. Storing it on your own backend (encrypted) is the safer alternative.
- If you take the simple route (browser talks straight to the AI), the key is on display. Anyone who opens their browser's developer tools and watches what's being sent can read it. That's just how browsers work — they don't hide what they're sending.
- Won't pass an enterprise security review. Big buyers want audit logs (who used which key when) and security certifications. BYOK in a settings page has neither. Fine for personal tools; not for selling to a bank.
- The work lands on your user. They have to find the key in their account, paste it in, and remember to update it when it expires. Fine for a prototype. For paying customers, it's friction a competitor will use against you.
- Keys you can see leak in ways you didn't plan for. Screenshots. Screen recordings sent to support. Screen-share videos. A key visible in an input field shows up in all of them. A key tucked away in a config file at least stays hidden.
Prototypes. Personal dev tools. BYOK demos. Workshops. Anything where the key belongs to the user and the stakes are low-to-medium.
Production apps that use your company's keys. Anything an enterprise customer is paying for. Anything that needs an audit trail.
Setup friction is invisible in a plan and expensive in practice. It does not stop you building the thing. It quietly decides how many versions of it you try.