Here is a rule I now apply to every product I build: no AI key, no provider secret, no orchestration logic ever lives on the client. Not in the mobile app, not in the browser widget, not in a "temporary" debug build. The model lives behind my server, the client asks my server for things, and my server talks to the LLM. That single boundary decides more about an AI product's security, cost, and maintainability than the choice of model does.
This isn't a theoretical preference. It's the shape three of my products converged on independently: Rețetele Mele, a recipe app that imports from URLs and estimates nutrition with Gemini; OnboardingVoice, a voice assistant you embed in any SaaS; and Studiază Biblia, a Bible-study companion built around an AI engine. Different domains, different stacks, same architectural spine. This article is the pattern, with the real evidence from each.
The leak you can't take back
Start with the thing people get wrong first: shipping a provider key in the client.
If your mobile app holds the Gemini key, that key is extractable. Decompile the APK, sniff the traffic, dump the keychain — the level of effort varies, but a secret that ships to a device is a secret that has left your control. The same is true for a browser widget. OnboardingVoice embeds in arbitrary third-party SaaS apps with a single script tag. A widget that ships to sites you don't own is the worst possible place to put an OpenAI key, because now your key is sitting in the DOM of every customer's production site.
The blast radius isn't just one stolen key. It's an attacker running your paid model on their workload, billed to you, until you notice the spike. It's a key you have to rotate across every installed client, which for a mobile app means a release cycle and a wait for users to update. And rotating a key that's baked into a published app is not a hotfix; it's a migration.
Rețetele Mele's clearest design decision was the one I'd repeat verbatim: "no AI keys on the device." Phase 10 of that build was a deliberate cutover that moved all Gemini and YouTube calls server-side. The Android client doesn't know what model is being called or what the prompt is. It pastes a URL and asks the backend to do the work. That's the whole point.
The proxy is the product boundary
Once the keys are server-side, the client's job shrinks to "make authenticated requests to my API." The server becomes a gateway in front of the provider, and that gateway is where all the interesting control lives.
OnboardingVoice is the purest example because its hardest constraint is real-time voice. It does voice-to-voice over WebRTC by proxying the OpenAI Realtime API. The browser never holds an OpenAI credential; it opens a session against the Django backend, which uses Channels and WebSockets to broker the connection and shuttle events between the browser and the model. The client gets a short-lived, scoped session that my server minted and can revoke. The provider key stays in one place I control. (To be honest about status: OnboardingVoice is a prototype that runs locally end-to-end, and I'd treat the full real-time loop as partially proven rather than battle-tested. The architecture, though, is exactly the one I'd keep.)
The proxy pattern buys you four things you can't get with a client-side key:
- Authentication and authorization — the client authenticates to you, and you decide whether this user is allowed this call. Rețetele Mele uses email plus JWT with refresh rotation and Google Sign-In; the AI endpoints sit behind that auth, not behind a shared secret.
- Per-user quotas — you can throttle a single abusive user without touching anyone else. Rețetele Mele enforces per-user quotas in exactly this spot.
- One rotation point — change the key on the server, and every client is instantly using the new one. No app release, no migration.
- Observability — every call passes through your code, so every call can be logged, measured, and costed.
Don't block the request — queue the job
AI calls are slow and they fail in ways HTTP requests shouldn't. A nutrition estimate or a recipe extraction can take seconds; a provider can rate-limit you or time out. If you run that inline in a request handler, you've tied a user-facing response to an unpredictable third party.
So the second half of this pattern is: long AI work goes on a queue. Rețetele Mele runs recipe import through a Celery worker. Pasting a URL kicks off an async job that runs an extraction cascade — try JSON-LD structured data first, fall back to parsing the HTML, fall back to AI extraction — landing somewhere in the 2-to-10-second range. Nutrition estimation through Gemini is its own async job. The phone gets an instant local write into Room and a job that resolves later; it never sits on a spinner waiting for a model.
Celery plus Redis shows up in all three products for the same reason. Studiază Biblia uses it, OnboardingVoice uses it, Rețetele Mele uses it. The queue gives you retries, backpressure, and the ability to re-enqueue a failed job — Rețetele Mele's admin can re-run a job that didn't land. None of that is possible if the client is making the provider call directly.
A guard that runs before the model
Putting AI server-side also means you can put logic in front of the model that the model never gets a vote on. The sharpest example I've built is in Studiază Biblia.
Its AI companion, Parakletos, has a pastoral crisis-detection guard. A keyword check intercepts self-harm signals before any LLM call and short-circuits straight to hotline information. The model is never asked. That's only possible because the request flows through my orchestration layer first — there's a place to stand between the user and the LLM. If the model lived on the client, there would be no reliable choke point to enforce a safety rule like that. Server-side AI isn't only a security posture; it's where your ethics and your guardrails actually get to run.
Versioned prompts and observability
When the prompt lives on the server, it becomes an artifact you can manage instead of a string baked into a build.
Rețetele Mele's admin — built on django-unfold — exposes AI prompts as editable, immutable, versioned records, with cost and latency comparison across versions and the ability to roll back. That's the payoff of centralization stated plainly: I can change a prompt without shipping a new app, compare what version 4 costs versus version 3, and revert if a "better" prompt quietly got more expensive or slower. The prompt is config, not code, and it's observable.
This is the part teams skip and regret. A prompt that lives only in your head, or only in last week's git diff, gives you no way to answer "why did our token bill jump on Tuesday?" Version every prompt, attach cost and latency to each version, and keep the old ones. Studiază Biblia leans on the same instinct architecturally by splitting its AI into two Django apps: ai_assistant handles conversation persistence, while ai_orchestrator is the engine that decides what context to retrieve, how to build the prompt, and which model to call. Keeping orchestration in one named place is what makes it observable and swappable.
Cost control and multi-provider gateways
The bill is the other reason to keep AI server-side. Every call through your gateway is a call you can count, attribute to a user, and cap. Per-user quotas, async jobs you can throttle, and prompt versions tagged with cost — together these turn "we have no idea what this costs" into a number you watch.
The gateway is also where multi-provider strategy lives. Studiază Biblia runs on both OpenAI (GPT-4o-mini) and Anthropic, chosen inside the orchestrator. The client doesn't know or care which provider answered; it asked my server, and my server decided. That decoupling is the long-term insurance policy. Models get deprecated, prices change, a provider has a bad week — and because routing is a server-side decision, switching is a config change, not a client release. (For the broader topic of designing AI that knows when to abstain and how to gate quality, I dug into that separately in RAG that refuses to hallucinate.)
The shape, in one breath
Strip it down and the pattern is small:
- Keys and orchestration live server-side, always.
- The client makes authenticated requests; the server proxies the provider with short-lived sessions.
- Slow AI work runs on a queue (Celery + Redis), with retries and re-enqueue.
- Guards and safety rules run before the model, in your code.
- Prompts are versioned config with cost and latency attached.
- Provider choice is a server-side routing decision, not a client dependency.
I didn't design this top-down. It's what shipping recipe import, voice onboarding, and an AI Bible companion taught me, one cutover at a time. If you're building anything AI-shaped, the cheapest decision you'll make is also the most important: keep the keys off the device.
This is one thread in a larger story about building 11 products solo, where the same backend patterns show up again and again. If you want the next pattern article as it lands, the newsletter is where I send them first.