{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"the-key-never-goes-to-the-browser","title":"The key never goes to the browser","url":"https://addaly.com/learn/building-apps-with-ai/the-key-never-goes-to-the-browser","position":58,"of":87,"minutes":8,"answer":"Anything the browser has, every visitor has, so the model is called from your own server route and the browser only ever talks to you.","keyIdea":"Anything the browser has, every visitor has, so the model is called from your own server route and the browser only ever talks to you."},"body":{"format":"markdown","text":"## The feature everybody adds first\n\nOnce 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.\n\nThe 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.\n\n## Anything the browser has, the user has\n\nThe 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.\n\nOpen 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.\n\nThis 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.\n\n## The shape that works\n\nThe browser calls **your** server. Your server holds the key, calls the provider, and returns the result:\n\n```text\nbrowser  →  POST /api/summarise  →  your route  →  provider  →  your route  →  browser\n```\n\nThe 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.\n\nThe 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.\n\n## Free keys, and a model with no key at all\n\nEvery technique in this block has a free path.\n\n**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.\n\n> Figure: The shape that works. Steps: 1. Browser (Calls your own route; never sees the provider); 2. Your route (Session check, usage count, the key from a server-only variable); 3. Provider (Answers your server, not the visitor); 4. Back through you (Logged, capped, refusable). Open the Network tab and use every AI feature once. Every request should go to your own domain. One straight to a provider's host is a key in public, whether or not it works.\n\n**Groq** runs open models (Llama, Gemma and others) very fast, with a free tier rate-limited per minute.\n\n**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.\n\n**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.\n\nWhichever 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.\n\n## The one exception people cite\n\nSome 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.\n\n## A test to keep\n\nOpen 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."},"check":{"question":"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?","options":[{"text":"Open the Network tab, trigger the feature, and read the request headers, where the key appears in full in the Authorization header of a request to the provider.","correct":true},{"text":"Run the production build locally and check that the environment variable is set and readable, which confirms the key is being read correctly from the host's settings panel at build time.","correct":false,"tempting":"Confirms the key is present, which was never in doubt; the problem is where it ends up, and a build check does not show that."},{"text":"Check the provider's dashboard for usage, which will show that the requests are arriving from many different IP addresses rather than one.","correct":false,"tempting":"That is what the dashboard shows after the key has been copied and abused; the leak itself is visible before anyone abuses it."},{"text":"Sign in as a second user and confirm that the summaries are not shared between accounts, which would reveal a scoping problem.","correct":false,"tempting":"A real check for a different hole; it says nothing about whether the key is exposed."}]},"prev":{"slug":"when-the-data-gets-big-enough-to-notice","title":"When the data gets big enough to notice","url":"https://addaly.com/learn/building-apps-with-ai/when-the-data-gets-big-enough-to-notice"},"next":{"slug":"the-first-call-from-your-own-server","title":"The first call from your own server","url":"https://addaly.com/learn/building-apps-with-ai/the-first-call-from-your-own-server"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The key never goes to the browser', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/the-key-never-goes-to-the-browser (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The key never goes to the browser","courseTitle":"Building Apps with AI","module":{"index":7,"slug":"a-model-inside-your-app","title":"Putting a model inside your own app"},"answer":null,"keyIdea":"Anything the browser has, every visitor has, so the model is called from your own server route and the browser only ever talks to you.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure 7.1","figure":{"type":"flow","steps":[{"label":"Browser","detail":"Calls your own route; never sees the provider"},{"label":"Your route","detail":"Session check, usage count, the key from a server-only variable"},{"label":"Provider","detail":"Answers your server, not the visitor"},{"label":"Back through you","detail":"Logged, capped, refusable"}],"title":"The shape that works","caption":"Open the Network tab and use every AI feature once. Every request should go to your own domain. One straight to a provider’s host is a key in public, whether or not it works."},"alt":"Figure: The shape that works. Steps: 1. Browser (Calls your own route; never sees the provider); 2. Your route (Session check, usage count, the key from a server-only variable); 3. Provider (Answers your server, not the visitor); 4. Back through you (Logged, capped, refusable). Open the Network tab and use every AI feature once. Every request should go to your own domain. One straight to a provider’s host is a key in public, whether or not it works."}},"blocks":[{"t":"h2","id":"the-feature-everybody-adds-first","runs":[{"text":"The feature everybody adds first"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"anything-the-browser-has-the-user-has","runs":[{"text":"Anything the browser has, the user has"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"the-shape-that-works","runs":[{"text":"The shape that works"}]},{"t":"p","runs":[{"text":"The browser calls "},{"text":"your","b":true},{"text":" server. Your server holds the key, calls the provider, and returns the result:"}]},{"t":"code","lang":"text","label":"Text","text":"browser  →  POST /api/summarise  →  your route  →  provider  →  your route  →  browser"},{"t":"p","runs":[{"text":"The 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."}]},{"t":"p","runs":[{"text":"The rule for the rules file is one line: "},{"text":"the model is called only from server code, and the key has no public prefix.","b":true},{"text":" The security block later adds the test for the built bundle that catches the case where the rule was ignored."}]},{"t":"h2","id":"free-keys-and-a-model-with-no-key-at-all","runs":[{"text":"Free keys, and a model with no key at all"}]},{"t":"p","runs":[{"text":"Every technique in this block has a free path."}]},{"t":"p","runs":[{"text":"Google AI Studio","b":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"Groq","b":true},{"text":" runs open models (Llama, Gemma and others) very fast, with a free tier rate-limited per minute."}]},{"t":"p","runs":[{"text":"OpenRouter","b":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"Ollama","b":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"the-one-exception-people-cite","runs":[{"text":"The one exception people cite"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"a-test-to-keep","runs":[{"text":"A test to keep"}]},{"t":"p","runs":[{"text":"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 "},{"text":"api.openai.com","code":true},{"text":", "},{"text":"generativelanguage.googleapis.com","code":true},{"text":" or any provider’s host, straight from the page, is a key in public, whether or not it works."}]}],"check":{"question":[{"text":"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?"}],"options":[{"text":[{"text":"Open the Network tab, trigger the feature, and read the request headers, where the key appears in full in the Authorization header of a request to the provider."}],"correct":true,"tempting":null},{"text":[{"text":"Run the production build locally and check that the environment variable is set and readable, which confirms the key is being read correctly from the host’s settings panel at build time."}],"correct":false,"tempting":[{"text":"Confirms the key is present, which was never in doubt; the problem is where it ends up, and a build check does not show that."}]},{"text":[{"text":"Check the provider’s dashboard for usage, which will show that the requests are arriving from many different IP addresses rather than one."}],"correct":false,"tempting":[{"text":"That is what the dashboard shows after the key has been copied and abused; the leak itself is visible before anyone abuses it."}]},{"text":[{"text":"Sign in as a second user and confirm that the summaries are not shared between accounts, which would reveal a scoping problem."}],"correct":false,"tempting":[{"text":"A real check for a different hole; it says nothing about whether the key is exposed."}]}],"order":[3,1,0,2],"why":null},"prev":{"slug":"when-the-data-gets-big-enough-to-notice","title":"When the data gets big enough to notice","minutes":8},"next":{"slug":"the-first-call-from-your-own-server","title":"The first call from your own server","minutes":9}}}