Field note
Static sites, edge functions, and the secret that almost shipped
A `.gitignore` is not a security model. Here is what actually keeps an API key off a public website, and the mistake that taught me.
In short
To keep an API key off a static website, never reference it from client code — put a server or edge function between the browser and the provider, and read the key from the environment at request time. A `.gitignore` entry only protects your git history; it does nothing about a key that gets inlined into the JavaScript bundle and served to every visitor.
I wanted a small AI concierge on my portfolio. The model provider issues API keys, so the first question was where the key lives. My repo already had a correct `.gitignore`, and my instinct was that this was solved.
The mistake: trusting `.gitignore` as a security model
A `.gitignore` prevents a file from being committed. It does absolutely nothing about what ends up inside your built JavaScript. On a static build, any environment variable your client code touches gets inlined at build time and shipped to every visitor in the bundle — readable in seconds from devtools.
The fix: build the boundary the architecture lacks
This site builds with a static adapter. There is no application server, so there is nowhere in the app to hold a secret. That is the real constraint, and it is easy to miss because nothing errors — the site builds fine and the key quietly ships.
The solution is an edge function: a small server-side handler deployed next to the static assets. The browser posts to it, it reads the key from the environment, calls the provider, and returns only the text. The key never crosses into client code.
// The whole security model in one line:
const apiKey = env.GROQ_API_KEY; // read at request time, server-side
// The browser only ever sees this:
return json({ content }); // no key, no provider authDefence in depth, because one layer is not enough
- .env stays gitignored, and the repo also carries a committed .env.example with empty placeholders so the shape is documented without the value.
- Production secrets live in the host dashboard, never in the repository at all.
- Verify the build output — grep the compiled bundle for the key prefix. If it appears, the boundary is broken.
- Clamp anything a caller can influence (token limits, body size) before it reaches a billable upstream.
- Rate-limit per client, and pass the provider’s own rate-limit response through honestly rather than hiding it behind a generic error.
I scanned the built output for the key I had just added, and it was absent. That single check is what turned "I think this is fine" into "this is verified".
The lesson worth keeping
Security that depends on a file staying untracked is a habit, not a guarantee. Security that depends on an architectural boundary holds even when someone is in a hurry. Build the boundary, then verify it by inspecting the artifact you actually shipped.
More Keep reading
Practice · 2 minBuilding a website with AI in 2026: the workflow that actually holds up
Coding agents are good enough to build real sites now. The skill is not prompting — it is supervising a loop, and knowing where to stop trusting.
Explainer · 2 minSEO when there is no click: being the source AI answers with
Answer engines synthesise instead of linking. What that rewards instead of keywords — and what quietly stopped working.