The feature everybody adds first
Once an app works, the next request is nearly always the same: “add an AI feature”. Summarise the note, suggest a reply, categorise the expense, write the description. The model behind it is reached through an HTTP call with a key, and the question this whole block turns on is where that call is made from.
The generated version, more often than not, makes it from the browser. A component calls the provider directly, with the key read from a variable, one that has the public prefix from the shipping block, because that is the only way a browser component can read it. It works on the first try, which is why it ships.
Anything the browser has, the user has
The browser is the user’s machine. Every byte your app sends there (HTML, JavaScript, the variables baked into it, the requests it makes) belongs to whoever is sitting at it, and reading it takes no skill at all.
Open the developer tools, choose the Network tab, trigger the feature, and click the request to the provider. The Authorization header is there, in full. Copy it, and you can make requests on that account from anywhere until the key is revoked. Bots do exactly this to public sites, automatically, and the first sign is a bill.
This is not a Next.js or React problem, and it is not fixed by obscuring the variable name. It is a property of the browser. A key in a browser is a key in public.
The shape that works
The browser calls your server. Your server holds the key, calls the provider, and returns the result:
browser → POST /api/summarise → your route → provider → your route → browserThe key lives in a server-only environment variable, never with a public prefix. The browser never sees the provider’s address. Your route can check the session, count usage, refuse abuse and log what happened, none of which a browser call could do, because a browser call is under the user’s control and a server route is under yours.
The rule for the rules file is one line: the model is called only from server code, and the key has no public prefix. The security block later adds the test for the built bundle that catches the case where the rule was ignored.
Free keys, and a model with no key at all
Every technique in this block has a free path.
Google AI Studio issues a key for Gemini models with a free tier limited by requests per minute and per day; enough to build and test with, and for a small app’s real use.
Groq runs open models (Llama, Gemma and others) very fast, with a free tier rate-limited per minute.
OpenRouter is one key in front of many providers and lists models marked free, with limits, which also makes switching providers a one-line change later.
Ollama runs a model on your own machine with no key and no network. A laptop with 8 GB of memory runs a 3 or 4 billion parameter model well enough to develop against; the quality is below the hosted models and the cost is zero, which is the right trade for the first week. It speaks the same API shape as the hosted providers, so code written against it moves to a hosted model by changing a URL.
Whichever you use, put a spending limit on the account before the key is created (most dashboards have one), so that the worst a mistake can cost is the number you typed.
The one exception people cite
Some providers offer short-lived, browser-safe tokens that your server mints per session for a realtime voice or streaming feature. Those exist, they are scoped and expire in minutes, and they are still issued by your server. The principle holds: the browser gets a credential your server chose to give it, for a purpose your server limited, never the account key.
A test to keep
Open the Network tab and use every AI feature once, watching where the requests go. Every one should go to your own domain. A request to api.openai.com, generativelanguage.googleapis.com or any provider’s host, straight from the page, is a key in public, whether or not it works.
Every lesson here is open. Your progress is recorded once you pass the Building with AI exam.
Check your understanding
A generated ‘summarise this note’ feature calls the model provider directly from the React component, with the key read from an environment variable. It works. What is the two-minute test that shows the problem, and what does it find?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Beatriz, your AI tutor