How to Keep API Keys Safe in an AI-Generated App
API keys make it easy to add powerful services to an application.
A few lines of code can connect your project to an AI model, email provider, payment service, mapping platform, cloud storage system, or analytics API. This convenience is one reason API integrations appear frequently in vibe-coded applications.
It is also why API keys are easy to mishandle.
A developer asks an AI coding assistant to “connect this form to an AI API,” and the assistant adds a key directly to a React component. The feature works locally, so the change appears successful. After deployment, however, the key is included in the browser’s JavaScript bundle, where anyone can inspect and reuse it.
Other keys are accidentally committed in .env files, pasted into AI prompts, displayed in screenshots, printed in deployment logs, included in URLs, or shared across development and production.
A leaked API key can cause more than a technical error. Depending on the provider and the credential’s permissions, someone may be able to consume paid resources, send messages under your account, read or modify data, upload files, or access other enabled services.
This guide explains how to design an application so that private API keys remain on trusted infrastructure. It also covers permission restrictions, Git protection, usage controls, monitoring, rotation, and incident response.
The examples continue with the appointment-booking application used throughout this series:
Frontend:
- React
- Vite
- Public browser application
Backend:
- Node.js
- Express
- Trusted server environment
Database:
- PostgreSQL
External APIs:
- Transactional email service
- AI service for drafting appointment summariesThe code is illustrative and has not been tested against your application or a specific API provider.
What an API Key Does
An API key is a credential that a service uses to identify or authorize an application making a request.
A server request may include the key in an authorization header:
Authorization: Bearer YOUR_API_KEYThe provider examines the credential and decides whether to accept the request.
Depending on the service, the key may determine:
- Which account or project receives the usage
- Which APIs can be called
- Which resources can be accessed
- Which rate limits apply
- Which billing account is charged
- Which activity appears in provider logs
An API key is not always sufficient for user authentication or high-value authorization. OWASP advises against relying exclusively on API keys to protect sensitive or critical resources because a key often identifies the calling application rather than a specific human user.
Your application may therefore need two different controls:
User authentication:
Who is making this request to your app?
Provider credential:
Which trusted application is calling the external API?For example, an administrator signs in to your appointment app. Your backend verifies the administrator’s identity and permission. Only then does the backend use its private email-provider key to send a message.
The API key does not replace your application’s authorization rules.
Private and Publishable Keys Are Different
Not every value called a “key” has the same confidentiality requirement.
Some providers intentionally offer publishable identifiers for browser or mobile use. These values may identify an account or project but are designed to work with additional server-side controls.
Other credentials are private and must never be delivered to untrusted clients.
Common private credentials include:
- AI service API keys
- Transactional email API keys
- Database credentials
- Cloud service secret keys
- Payment-provider secret keys
- Administrative access tokens
- Private signing keys
- Service-account credentials
Common public or publishable values may include:
- Public analytics identifiers
- Map keys restricted to approved domains and APIs
- Payment-provider publishable keys
- Public project identifiers
- Client IDs used in an OAuth flow
A name such as PUBLIC_KEY does not prove that a value is safe to expose. Read the provider’s official documentation and confirm the intended environment.
When uncertain, treat the credential as private until the provider explicitly documents browser or mobile use.
The Core Architecture: Browser Calls Your Server
A private API key should normally stay on a backend you control.
The unsafe pattern is:
Browser
|
| Private API key
v
External AI or email APIThe browser must receive the key to make this request. Once delivered, the value can be inspected through source files, bundled JavaScript, browser extensions, network requests, or device-level tools.
The safer pattern is:
Browser
|
| Authenticated, validated request
v
Your backend
|
| Private API key
v
External APIOpenAI’s official API-key safety guidance states that API keys should not be deployed in browsers or mobile applications. Requests should be routed through your own backend so the credential remains on trusted infrastructure.
The same architecture applies to many other private APIs.
An Unsafe Frontend Integration
An AI coding assistant might generate code like this:
const apiKey = import.meta.env.VITE_AI_API_KEY;
export async function generateSummary(appointment) {
const response = await fetch("https://provider.example/v1/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify({
input: appointment
})
});
return response.json();
}This is unsafe for a private credential.
Vite exposes variables with the VITE_ prefix to client-side code during the build. The generated value becomes part of the browser application. Minification does not protect it.
The correct response is not to rename the variable:
PRIVATE_AI_API_KEY=...A different name does not solve the architectural problem if the frontend still needs the value.
The integration must move to the backend.
A Safer Backend Integration
The frontend calls your application’s API:
export async function generateAppointmentSummary(appointmentId) {
const response = await fetch(
`${import.meta.env.VITE_API_URL}/api/appointments/${appointmentId}/summary`,
{
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json"
}
}
);
if (!response.ok) {
throw new Error("The summary could not be generated.");
}
return response.json();
}The browser receives only the public address of your backend.
The Express backend validates the user and loads the record:
app.post(
"/api/appointments/:appointmentId/summary",
requireAuthenticatedUser,
async (req, res, next) => {
try {
const appointmentId = req.params.appointmentId;
const appointment = await appointments.findAuthorizedAppointment({
appointmentId,
userId: req.user.id
});
if (!appointment) {
return res.status(404).json({
error: "Appointment not found."
});
}
const summary = await summaryService.generate({
appointmentId: appointment.id,
serviceName: appointment.serviceName,
scheduledAt: appointment.scheduledAt
});
return res.status(200).json({ summary });
} catch (error) {
next(error);
}
}
);The server-side integration reads the key from the trusted environment:
function requireEnvironmentVariable(name) {
const value = process.env[name];
if (!value || value.trim() === "") {
throw new Error(`${name} is required`);
}
return value;
}
const aiApiKey = requireEnvironmentVariable("AI_API_KEY");Then it calls the provider:
export async function generate(input) {
const response = await fetch(process.env.AI_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${aiApiKey}`
},
body: JSON.stringify({
input
})
});
if (!response.ok) {
throw new Error(`AI provider returned status ${response.status}`);
}
return response.json();
}The backend must still enforce:
- Authentication
- Authorization
- Input validation
- Request-size limits
- Rate limits
- Usage limits
- Timeout handling
- Safe logging
- Error handling
Otherwise, an attacker may not need to steal the key. They may be able to abuse your public backend as an unrestricted proxy.
A Server Proxy Is Not Automatically Safe
Moving an API key to the backend solves the browser-exposure problem, but it can create a new risk if the endpoint accepts unlimited requests from anyone.
Consider this route:
app.post("/api/ai", async (req, res) => {
const result = await callAiProvider(req.body.prompt);
res.json(result);
});Problems include:
- No authentication
- No user authorization
- No prompt-length limit
- No request rate limit
- No usage quota
- No timeout
- No restriction on supported operations
- No ownership or account check
- No audit event
- No protection against automated abuse
Someone could repeatedly call your endpoint and cause provider usage under your account.
A safer endpoint should expose a narrow business action rather than a general-purpose API proxy.
Prefer:
POST /api/appointments/:id/summaryover:
POST /api/provider-proxyThe narrow route allows the server to control the input, retrieve trusted data, limit output, check ownership, and apply a specific quota.
Validate and Minimize Data Sent to External APIs
Do not forward an entire database record to an AI or external service merely because the coding assistant used:
body: JSON.stringify(appointment)An appointment record may include data the provider does not need:
- Internal database IDs
- Customer email addresses
- Phone numbers
- Administrative notes
- Authentication metadata
- Internal tags
- Audit information
- Payment references
Create an explicit provider payload:
const providerInput = {
serviceName: appointment.serviceName,
scheduledAt: appointment.scheduledAt,
customerRequest: appointment.customerRequest
};Then decide whether customerRequest may contain personal or sensitive information. Minimize or remove data that is not required.
This is both a privacy practice and a security practice. If provider logs, application logs, or debugging output are later exposed, less unnecessary data is affected.
Never Put Private Keys in URLs
Avoid sending a private key through a URL query parameter:
https://api.example.com/generate?api_key=PRIVATE_KEYURLs can appear in:
- Browser history
- Reverse-proxy logs
- Web-server logs
- Analytics systems
- Monitoring platforms
- Error reports
- Referrer information
- Screenshots
- Support messages
OWASP specifically advises against placing API keys in URLs and recommends sending credentials through headers when the API supports it.
Use the authentication mechanism documented by the provider. A common pattern is:
Authorization: Bearer PRIVATE_KEYHeaders can still leak when request logging is misconfigured. Redact or omit authorization headers from logs.
Create a Separate Key for Each Environment
Do not reuse one API key across development, staging, production, personal scripts, and automated tests.
Use separate credentials such as:
Development:
ai-appointments-development
Staging:
ai-appointments-staging
Production:
ai-appointments-productionSeparate keys provide several benefits:
- A development leak does not automatically compromise production.
- Usage can be attributed to an environment.
- A staging key can have lower limits.
- One environment can be rotated without interrupting the others.
- Production access can have stricter permissions.
- Unused credentials can be revoked independently.
Do not include the environment only in your local variable name while using the same provider credential underneath. Create genuinely separate credentials at the provider when supported.
Use a Separate Key for Each Application
Sharing one credential across unrelated applications makes incident response harder.
Suppose three projects use the same email key:
Appointment app
School project
Portfolio contact formWhen unusual email activity appears, you may not know which application is responsible. Rotating the key breaks all three.
A separate key per application allows you to:
- Attribute usage
- Apply specific restrictions
- Set different quotas
- Revoke one integration
- Identify inactive applications
- Review permissions more easily
Use clear provider-side names that do not contain sensitive customer information:
appointment-production-email
appointment-staging-email
appointment-production-aiRestrict What Each Key Can Do
When the provider supports restrictions, configure them.
Possible restrictions include:
- Allowed APIs
- Permitted operations
- Source IP addresses
- HTTP referrer domains
- Mobile application identifiers
- Resource scopes
- Sending domains
- Geographic regions
- Rate or usage quotas
- Expiration dates
Google Cloud’s official guidance recommends applying both application restrictions and API restrictions to API keys where applicable. It also recommends using separate keys for separate applications and deleting keys that are no longer needed.
Restrictions should match where the key is used.
A browser-intended map key might use approved-domain restrictions. A server-side credential might use a fixed outbound IP when the hosting architecture supports one. An email key might be limited to sending from one verified domain.
Restrictions are defense in depth. They do not make it acceptable to publish a private key.
Prefer Short-Lived Credentials Where Available
Some platforms support temporary credentials or identity-based access instead of long-lived static keys.
Examples include:
- OAuth access tokens
- Workload identity
- OpenID Connect federation
- Cloud service roles
- Automatically rotated service credentials
OWASP notes that identity-based and passwordless mechanisms can reduce reliance on manually managed long-lived secrets.
For a beginner project, a normal restricted API key may be the only practical option. When the provider supports trusted platform identities, however, evaluate whether the application can authenticate without storing a long-lived key.
Short-lived credentials limit how long a stolen value remains useful. They also require correct renewal, permission, and failure handling.
Keep Keys Out of Git
Your repository should contain the variable name, not the real value.
Safe example:
const emailApiKey = process.env.EMAIL_API_KEY;Safe .env.example entry:
EMAIL_API_KEY=replace-with-provider-keyUnsafe source:
const emailApiKey = "real-provider-key";Unsafe committed configuration:
EMAIL_API_KEY=real-provider-keyEnsure .gitignore covers local environment files:
.env
.env.*
!.env.exampleCheck which files are tracked:
git ls-files | grep -E '(^|/)\.env'On PowerShell:
git ls-files | Select-String '(^|/)\.env'Remember that .gitignore does not remove an already tracked file and does not erase previous commits.
Enable Secret Scanning and Push Protection
Automated secret scanning can detect some recognizable credential formats.
GitHub push protection examines pushes for supported secret patterns and can block a credential before it reaches the repository. GitHub warns that detection depends on supported patterns, so it should not be treated as a complete guarantee that every secret will be found.
Review the repository’s security settings and enable available protections.
When push protection blocks a key:
- Do not bypass the warning merely to finish the push.
- Remove the credential from the commit.
- Store it in the correct secret system.
- Rotate it if it may already have been exposed elsewhere.
- Check earlier commits and branches.
- Push the cleaned change.
A bypass option exists for exceptional cases, but convenience is not a valid reason to publish a real credential. GitHub records bypass activity and can generate alerts when protection is bypassed.
Automated scanning should supplement:
- Manual diff review
- Local secret scanning
- Protected branches
- Focused commits
- Repository access control
- Credential rotation
- Provider usage monitoring
Review the Git Diff Before Every Push
A simple review can catch a key before it leaves your computer:
git status --short
git diff
git diff --stagedLook for:
- Long unfamiliar strings
- Authorization headers
.envfiles- Debug configuration
- Database URLs
- Provider response objects
- Test fixtures copied from live requests
- Screenshots or text files containing credentials
AI coding assistants may edit more files than the user intended. Always examine the complete diff, not only the file mentioned in the assistant’s summary.
Avoid broad staging until you understand every file:
git add .Instead, stage reviewed files:
git add server/src/services/summary-service.js
git add server/src/routes/appointments.js
git add .env.exampleThen inspect the staged diff again.
Do Not Paste API Keys Into AI Prompts
A coding assistant usually does not need a real key to:
- Generate integration code
- Review a request format
- Create environment-variable validation
- Explain provider authentication
- Write tests
- Diagnose an HTTP status
- Prepare deployment configuration
Use placeholders:
AI_API_KEY=replace-with-provider-keyProvide a redacted error:
Authorization: Bearer [REDACTED]
Provider returned HTTP 401.Do not paste:
- Complete environment files
- Deployment dashboard screenshots containing values
- Terminal output containing credentials
- Raw request headers
- Full database URLs
- Provider account exports
- Service-account files
When sharing code for review, search it first.
The assistant may also generate examples containing key-like strings. Ensure examples are unmistakably fictional and cannot be mistaken for active credentials.
Keep Keys Out of Logs and Error Reports
Avoid:
console.log("Using API key", process.env.AI_API_KEY);Avoid logging complete request configuration:
console.log({
url,
headers,
body
});The headers object may contain the authorization credential.
Log safe metadata:
logger.info("AI summary request completed", {
appointmentId,
providerStatus: response.status,
durationMs
});When logging provider failures, include:
- Provider name
- Operation
- HTTP status
- Request ID supplied by the provider
- Duration
- Internal correlation ID
Exclude:
- API key
- Authorization header
- Full customer payload
- Provider request body containing personal data
- Raw provider response when it may contain sensitive information
Some error-reporting tools automatically capture request headers or environment details. Review their data-scrubbing configuration before enabling them in production.
Add Timeouts and Controlled Retries
An external API request can hang, fail temporarily, or return a rate-limit response.
Use a timeout:
export async function requestSummary(payload) {
const response = await fetch(process.env.AI_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AI_API_KEY}`
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15_000)
});
if (!response.ok) {
throw new Error(`Provider request failed with ${response.status}`);
}
return response.json();
}Confirm that AbortSignal.timeout is supported by the runtime version used in your deployment.
Retries should be limited and applied only to failures that may succeed later. Repeating every failed request can multiply cost and load.
Do not automatically retry:
- Invalid authentication
- Invalid input
- Permission denial
- Clearly permanent errors
Use caution with operations that have side effects. Retrying an email or payment request may duplicate the action unless the provider supports idempotency controls.
Add Application-Level Rate Limits
Provider quotas protect the provider account, but your backend also needs limits appropriate for your users.
Possible controls include:
- Requests per user
- Requests per IP address
- Requests per appointment
- Daily AI summaries per account
- Maximum prompt or payload size
- Maximum output length
- Administrative approval for expensive operations
- Queue limits
- Concurrency limits
Rate limits based only on IP addresses can affect multiple legitimate users sharing one network. Authenticated user and account limits are often more meaningful when accounts exist.
Store rate-limit data in a shared system when multiple backend instances run. An in-memory counter on one process does not provide a complete limit across several instances.
Rate limiting is not authorization. A user must still be allowed to perform the requested action.
Configure Provider Budgets, Quotas, and Alerts
When supported, set provider-side safeguards:
- Request quotas
- Token or usage limits
- Spending notifications
- Billing alerts
- Per-project budgets
- Per-key restrictions
- Usage dashboards
- Anomaly alerts
These controls reduce risk and help you detect unusual activity.
They do not replace key security. A billing alert may arrive after usage has already occurred, and a budget notification may not automatically stop requests.
Create limits based on expected usage rather than leaving a new integration unrestricted.
For example:
Expected initial usage:
- 20 test users
- Up to 5 summaries per user per day
- Maximum 100 summary requests per day
- Alert when usage reaches an unusual thresholdDo not copy these numbers into your app without estimating your own traffic, output sizes, retries, and provider pricing.
Monitor Key Usage
Review provider activity regularly, especially after deployment.
Useful signals include:
- Sudden request spikes
- Requests outside expected hours
- Usage from unexpected regions or IP addresses
- Calls to APIs the application does not use
- Repeated authentication failures
- Unusual output volume
- Increased error rates
- Unexpected cost growth
Give every application and environment a separate credential so these signals can be attributed.
Create an internal record:
Credential name:
appointment-production-ai
Owner:
Application operations
Used by:
summary-worker
Environment:
production
Permissions:
summary generation only
Created:
[date]
Last rotated:
[date]
Expected usage:
[documented range]
Revocation location:
[provider console area]
Dependent service:
appointment-apiDo not include the credential value in this inventory.
Rotate Keys Safely
Rotation replaces an existing credential with a new one.
When the provider allows both keys to remain active temporarily:
- Create a new restricted key.
- Add it to the staging environment.
- Verify the integration.
- Update the production secret.
- Restart or redeploy the affected service.
- Run a controlled production test.
- Confirm requests use the new credential.
- Revoke the old key.
- Monitor for use of the old key.
- Record the rotation.
When only one active credential is allowed, prepare a short maintenance procedure and clear rollback steps.
Do not rotate every key at once. Separate changes make failures easier to diagnose.
The variable name can remain the same:
AI_API_KEYThe provider-issued value changes.
What to Do When a Key Is Exposed
Treat an exposed private key as compromised.
Do not wait to see whether someone uses it.
Take these steps:
- Revoke or disable the exposed key at the provider.
- Create a new restricted credential.
- Update the affected service through its secret-management system.
- Redeploy or restart the service.
- Verify the application using the replacement.
- Review provider activity and billing.
- Search source code, Git history, logs, build artifacts, screenshots, and tickets.
- Remove the value from current files.
- Rewrite Git history when appropriate.
- Document the cause and prevention changes.
Revocation comes before repository cleanup because removing the string from Git does not prevent someone from using a copied credential.
OpenAI’s official guidance advises rotating a key immediately when it may have been leaked and reviewing usage for unexpected activity.
Do not expose the key again in an incident report. Refer to it by a safe provider-side name or the last few characters only when necessary.
Common API-Key Mistakes in AI-Generated Apps
Adding the key to a React component
Anything delivered to the browser is public.
Move the provider call to the backend.
Renaming the frontend variable
Changing VITE_API_KEY to SECRET_API_KEY does not help when client code still requires it.
Change the architecture, not only the name.
Creating an unrestricted backend proxy
An open proxy allows other people to consume the API through your server.
Expose narrow, authenticated business actions.
Sharing one key across every project
A leak affects every application and makes attribution difficult.
Create separate keys.
Giving a key full account permissions
The application may need only one API or operation.
Apply least privilege and provider restrictions.
Committing the key to a private repository
Private repositories reduce public visibility but do not make committed credentials safe. Collaborators, integrations, compromised accounts, copied archives, and future visibility changes can expose them.
Keep secrets out of Git.
Deleting a key from the latest commit only
The credential may remain in previous commits, branches, build caches, logs, or clones.
Revoke it first and review its complete exposure.
Relying only on secret scanning
Scanners detect supported patterns and can miss custom or transformed credentials.
Review diffs and monitor provider usage.
Logging provider request headers
The authorization header may contain the key.
Redact headers and log safe metadata.
Assuming a spending alert stops usage
Alerts may only notify you.
Use multiple controls: restricted keys, application limits, quotas, monitoring, and rapid revocation.
AI Prompt for Reviewing API-Key Safety
Goal:
Review this application for API-key exposure and abuse risks.
Project:
- React and Vite frontend
- Node.js and Express backend
- PostgreSQL database
- External AI API
- Transactional email API
- Render deployment
Constraints:
- Do not request or print real credentials.
- Treat all VITE_-prefixed values as public.
- Do not move private keys into frontend code.
- Do not create a general-purpose provider proxy.
- Do not log authorization headers.
- Do not change provider accounts or revoke keys.
- Do not add dependencies without explaining why.
- Do not assume a private repository is a secret store.
- Identify findings before modifying files.
Review:
1. API-key references in source and configuration
2. Keys exposed through frontend builds
3. Hardcoded credentials
4. Tracked environment files
5. Keys in URLs or request logs
6. Unauthenticated provider endpoints
7. Missing authorization checks
8. Missing input and output limits
9. Missing application-level rate limits
10. Overly broad provider permissions
11. Shared development and production credentials
12. Unsafe retry behavior
13. Provider usage monitoring gaps
14. Rotation and incident-response readiness
Expected output:
- Findings ranked by severity
- Exact affected files
- Explanation of each exposure path
- Proposed changes without applying them
- Verification steps that do not reveal credentials
- Items requiring provider-console reviewUse placeholders when giving code to an AI assistant. Review the complete Git diff before applying any recommendation.
API-Key Security Checklist
- Every external API credential has a documented owner.
- Each credential is classified as private or intentionally publishable.
- Private API keys are never delivered to browser code.
- No private key uses a frontend-exposure prefix such as
VITE_. - Browser requests requiring private credentials go through the backend.
- Backend provider routes require appropriate authentication.
- Backend provider routes enforce authorization.
- Provider routes expose narrow business actions rather than a general proxy.
- Input length and format are validated.
- Unnecessary personal data is removed before provider requests.
- Private keys are sent through provider-approved authentication mechanisms.
- Private keys are not placed in URL query parameters.
- Development, staging, and production use separate credentials.
- Unrelated applications use separate credentials.
- Each key has the minimum required permissions.
- Provider-side API restrictions are enabled where available.
- Provider-side application restrictions are enabled where appropriate.
- Unused credentials are removed.
- Long-lived keys are replaced with temporary identity mechanisms where practical.
- Real credentials are not stored in source files.
- Real credentials are not stored in Git-tracked environment files.
.env.examplecontains placeholders only.- The complete Git diff is reviewed before each push.
- Secret scanning and push protection are enabled where available.
- Push-protection warnings are not bypassed for convenience.
- Real credentials are not pasted into AI prompts.
- Deployment screenshots do not reveal credentials.
- Logs do not include authorization headers.
- Logs do not include complete environment objects.
- Error reports do not expose provider request configuration.
- External requests use appropriate timeouts.
- Retry behavior is limited and reviewed for duplicate side effects.
- Application-level rate limits protect paid provider operations.
- User or account usage limits are defined.
- Provider quotas and alerts are configured where available.
- Provider usage is reviewed for unusual activity.
- A credential inventory records names and ownership without values.
- Each key has a documented rotation procedure.
- Exposed keys can be revoked without searching for instructions during an incident.
- Credential revocation happens before Git-history cleanup.
- Replacement keys are verified before old keys are removed when overlap is supported.
- API-key security is rechecked after AI-generated integration changes.
