Last updated: August 6, 2026
Do not put a long-lived OpenAI API key in a browser, React bundle, Vite variable, mobile app or public desktop package. If client-side JavaScript can send the key, a user can inspect it in DevTools, a bundled source file, a source map or the network request. Encoding, minification and CORS do not turn that credential into a secret.
The safe production pattern is: browser → your authenticated and rate-limited backend → model API. Your backend stores the provider key, validates a narrow request, selects an allowed model, caps output and records abuse-safe usage. A proxy that accepts arbitrary upstream URLs or anonymous unlimited prompts is still unsafe.
LumeAPI is an independent third-party OpenAI-compatible gateway. The same security rule applies to its keys: keep LUMEAPI_KEY on the server and call https://api.lumeapi.site/v1 only from trusted infrastructure.
Short path: Review the OpenAI-compatible API, select an allowlisted ID from models, then put the protected server route below between the browser and the gateway.
Quick Answer
| Approach | Is a long-lived model key safe? | Why |
|---|---|---|
VITE_OPENAI_API_KEY or NEXT_PUBLIC_* | No | Public-prefixed variables are shipped to the client bundle |
| Hardcoded or obfuscated JavaScript | No | The browser must recover the value to send it |
dangerouslyAllowBrowser: true | No for a shared production key | The SDK warning exists because users can extract the credential |
| CORS plus a browser key | No | CORS restricts browser behavior, not curl, scripts or stolen credentials |
| Server environment variable plus controlled proxy | Yes, with additional controls | The long-lived provider key never reaches the client |
| User supplies their own key for a local-only tool | Different risk model | The user's own credential still needs clear storage and exfiltration boundaries |
In short
A browser cannot both possess a reusable secret and hide it from the person controlling that browser. Move the provider key to a server. Then protect the proxy itself with user authentication, an endpoint-specific schema, model and token allowlists, request-size limits, rate limits, timeouts, logging redaction and spend monitoring. If a key has already been exposed, revoke it first; deleting it from the source repository does not invalidate copies.
Why frontend environment variables do not hide secrets
Build tools replace frontend environment references during compilation or expose them through a runtime configuration object. A user can then find the value in one or more places:
- Downloaded JavaScript bundles and source maps
- Browser DevTools Network request headers
- Browser extensions, injected scripts or compromised dependencies
- Error monitoring, analytics payloads, screenshots and copied logs
- Mobile or desktop packages that can be unpacked
The variable name is not the security boundary. VITE_, REACT_APP_ and NEXT_PUBLIC_* explicitly mark values for client use. A .env file protects a server secret only when its value remains in a server process and never enters the client build.
What the official SDK warning means
The official OpenAI Node.js library disables browser use by default and requires the explicit dangerouslyAllowBrowser option. Its documentation warns that enabling the option exposes secret API credentials in client-side code and may allow unauthorized requests and charges.
That flag is an acknowledgement of risk, not encryption. It may be reasonable for short-lived credentials in a controlled experiment or for a user who deliberately supplies their own key to a local-only application. It is not a safe way to ship one company key to every visitor.
The secure request boundary
Browser
└─ authenticated request with application session
└─ your backend /api/ai
├─ verify user/session
├─ rate-limit by user and IP
├─ validate prompt and request size
├─ choose from server-owned model allowlist
├─ cap output and timeout
└─ add LUMEAPI_KEY server-side
└─ https://api.lumeapi.site/v1/chat/completionsThe browser may know your /api/ai URL. Hiding an endpoint is not security. The backend must remain safe when a user calls it directly outside your webpage.
Install a narrow Node.js proxy
This Express example assumes your application already has authentication middleware that sets req.user.id. That is an intentional boundary: model proxy code should consume an authenticated identity, not invent a login system.
npm install express helmet express-rate-limit zod openaiStore the model key only in the server environment:
export LUMEAPI_KEY='your-lumeapi-key'
export LUMEAPI_MODEL='gpt-5.4-mini'Never prefix the secret with VITE_, NEXT_PUBLIC_ or another client-exposure convention.
Build the controlled proxy route
import express from 'express';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import OpenAI from 'openai';
import { z } from 'zod';
const apiKey = process.env.LUMEAPI_KEY;
if (!apiKey) throw new Error('LUMEAPI_KEY is missing');
const allowedModels = new Set(['gpt-5.4-mini']);
const defaultModel = process.env.LUMEAPI_MODEL ?? 'gpt-5.4-mini';
if (!allowedModels.has(defaultModel)) throw new Error('Unapproved model');
const client = new OpenAI({
apiKey,
baseURL: 'https://api.lumeapi.site/v1',
timeout: 30_000,
maxRetries: 1,
});
const app = express();
app.disable('x-powered-by');
app.use(helmet());
app.use(express.json({ limit: '20kb' }));
const aiLimit = rateLimit({
windowMs: 60_000,
limit: 12,
standardHeaders: 'draft-8',
legacyHeaders: false,
keyGenerator: (req) => req.user?.id ?? req.ip,
});
const RequestSchema = z.object({
prompt: z.string().trim().min(1).max(4_000),
}).strict();
function requireUser(req, res, next) {
if (!req.user?.id) return res.status(401).json({ error: 'sign_in_required' });
return next();
}
app.post('/api/ai', requireUser, aiLimit, async (req, res) => {
const parsed = RequestSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'invalid_request' });
}
try {
const result = await client.chat.completions.create({
model: defaultModel,
messages: [{ role: 'user', content: parsed.data.prompt }],
max_tokens: 400,
});
return res.json({
text: result.choices[0]?.message?.content ?? '',
requestId: result._request_id ?? null,
});
} catch (error) {
console.error('model_request_failed', {
status: error?.status ?? 500,
userId: req.user.id,
});
return res.status(502).json({ error: 'model_request_failed' });
}
});
app.listen(3000, () => console.log('Server listening on :3000'));Connect your real session middleware before requireUser. Do not copy a username from a browser header and treat it as authenticated identity. Behind a reverse proxy, configure trusted proxy handling carefully so an attacker cannot choose the IP used by the limiter.
Call only your backend from the browser
const response = await fetch('/api/ai', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: 'Summarize these release notes.' }),
});
if (!response.ok) {
throw new Error(`Application request failed: ${response.status}`);
}
const data = await response.json();
document.querySelector('#answer').textContent = data.text;The browser sends the application's authenticated session, preferably through a Secure, HttpOnly and appropriately scoped cookie. It never receives the LumeAPI key. Add CSRF protection when your authentication and cross-origin design require it.
A proxy is not secure until it limits abuse
| Control | Threat it reduces | Minimum production decision |
|---|---|---|
| Authentication | Anyone spending your balance | Require a real user or tenant identity |
| Per-user and per-IP rate limits | Automated request floods | Bound requests and tokens per time window |
| Model allowlist | User selects a premium or unsupported model | Keep model choice server-owned |
| Request schema and size cap | Arbitrary pass-through payloads | Accept only the fields the feature needs |
| Output cap | Unbounded completions and bills | Set a route-specific maximum |
| Timeout and retry cap | Hanging or multiplied attempts | Use one retry owner and total deadline |
| Content/feature policy | Proxy becomes a general-purpose relay | Restrict the endpoint to its product task |
| Usage attribution | Cannot identify an abusive user | Log user, route, model, outcome and token totals |
| Redaction | Logs become a second leak | Never log Authorization or full private prompts by default |
Do not accept a base_url from the browser and fetch it on the server. That creates a server-side request forgery risk. Do not accept an arbitrary model ID or pass every request field directly upstream.
Why CORS and obfuscation are not authentication
CORS tells cooperating browsers which origins may read a response. An attacker with a stolen key can call the provider from a server, command line or custom client that does not enforce your browser's CORS policy.
Minification changes formatting. Base64 changes representation. Encryption embedded in a browser bundle must also ship the decryption path or send a decryptable value. None prevents extraction when the browser needs the usable credential.
Domain allowlists can add defense in depth when a provider supports them, but request origins and referrers are not a substitute for keeping a billing credential server-side.
Test the proxy like an attacker
- Search the production bundles and source maps for key prefixes and the real credential.
- Inspect every browser request and confirm no provider Authorization header appears.
- Call
/api/aiwithout a session and require401. - Send an unknown field, oversized prompt and malformed JSON; require rejection.
- Attempt to supply another model or upstream URL; the schema must reject it.
- Exceed the user and IP budgets; require
429from your application. - Disconnect or exceed the timeout; confirm work stops and retry count stays bounded.
- Inspect logs, analytics and error reports for secrets and private prompt bodies.
- Confirm usage records map requests to users or tenants without storing unnecessary content.
- Rotate the provider key and prove the service can deploy the replacement safely.
This checklist is the page's second unique module: it validates the security property rather than merely drawing a proxy diagram.
Estimate the maximum abuse exposure
Rate limits should connect to a dollar budget. For a route with a maximum of 4,000 input and 400 output tokens, using the dated gpt-5.4-mini LumeAPI rates of $0.225/$1.35 per million:
maximum model cost per accepted request
= (4,000 × $0.225 + 400 × $1.35) / 1,000,000
= $0.00144At 12 requests per minute, one continuously active identity has a simplified ceiling of about $1.04 per hour. That is not a complete financial control: distributed accounts, retries, other routes and changed rates can raise exposure. Combine per-user limits with tenant budgets, global circuit breakers and usage alerts.
If the key is already exposed
- Revoke the key immediately; do not wait to confirm misuse.
- Review usage, unfamiliar models, timestamps, IP evidence and project limits.
- Create a replacement key with the smallest available scope.
- Put it in server secret storage and redeploy every legitimate consumer.
- Remove it from source, build artifacts, logs, tickets, screenshots and CI output.
- Treat Git history and cached browser bundles as still exposed.
- Add secret scanning and an ownership/rotation record.
Rewriting Git history can reduce accidental discovery but does not make the old key safe. Revocation is the security action.
A realistic production scenario
A developer builds a React support widget and places LUMEAPI_KEY in a Vite .env file. The build works, but the value is embedded into the downloaded JavaScript and visible in the Network Authorization header. Moving the value to an Express server stops direct exposure, yet the first proxy version remains anonymous and accepts any model.
The production fix adds the application's session middleware, a prompt-only schema, a standard-model allowlist, output cap, user/IP rate limits, a global budget alert and redacted request logging. The frontend calls only /api/ai. Security comes from the server enforcing who may spend what—not from hiding the URL.
What most guides get wrong
Many tutorials say “put the key in .env” without distinguishing server and client environments. In Vite or another browser build, an exposed variable still becomes public. Other tutorials suggest a serverless proxy but omit authentication, request constraints and spend controls, turning one protected key into an unlimited public relay.
The correct design protects both credentials and budget. A key can remain hidden while an unprotected proxy still lets attackers spend it.
Expert take
Assume anything delivered to a user-controlled device is public. Keep provider credentials in trusted server infrastructure, give the browser only an application session, and make the proxy narrower than the upstream API. The security test is not “Can I see the key?” It is “Can an unauthorized caller cause provider work or choose cost?” Both answers must be no.
FAQ
Can I hide an OpenAI API key in React or Vite .env?
No. Variables exposed to a client build are downloadable. Use a backend or serverless function that retains the provider key.
Is dangerouslyAllowBrowser safe?
It explicitly permits browser use despite credential exposure risk. Do not use it to ship one long-lived production key to untrusted users.
Does CORS protect a leaked API key?
No. CORS is enforced by browsers. A stolen key can be used from other clients and servers.
Is a backend proxy enough?
Not by itself. Authenticate users, validate a narrow schema, restrict models and output, rate-limit, monitor spend and redact logs.
What should I do after committing a key to GitHub?
Revoke it immediately, inspect usage, create a replacement and remove the secret from code and history. Removal without revocation is insufficient.
Sources and verification
- OpenAI API key safety guidance for the instruction not to deploy keys in browsers/mobile apps and to route requests through a backend.
- Official OpenAI Node.js SDK for the browser default and
dangerouslyAllowBrowserwarning. - OpenAI account security guidance for leak prevention, usage monitoring and rotation response.
- Community demand was reviewed across Reddit web-development threads, Stack Overflow questions, the OpenAI Node GitHub issue tracker and Chinese developer discussions. Repeated concerns were frontend
.envexposure, proxy abuse and leaked-key recovery. - LumeAPI OpenAI-compatible API, model catalog and usage logs guide for route-specific implementation and monitoring.
Both JavaScript blocks were syntax-checked on August 6, 2026. The Express sample intentionally depends on the application's existing authenticated req.user; it is not a complete identity provider. No live key or attack traffic was used.