
Preventing Data Exposure in Vibe Coding: Security Best Practices
Introduction
In the rapidly evolving landscape of vibe coding, where AI tools generate code based on natural language prompts, one critical security vulnerability consistently emerges: the exposure of sensitive credentials and API keys. This issue is particularly prevalent in full-stack app builders like Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit, where the convenience of rapid development can sometimes come at the cost of security.
According to a recent report by GitGuardian, repositories using AI coding tools show a 40% higher rate of secret exposure compared to traditional development. This alarming statistic highlights a significant security gap in vibe coding practices. When developers instruct AI to “connect to the database” or “integrate with Stripe,” the resulting code often includes hardcoded credentials directly in the source files—a practice that violates fundamental security principles.
The consequences of exposed credentials can be devastating: unauthorized access to databases, compromised user data, financial losses from abused API services, and potential regulatory violations. In one notable case, a startup using vibe coding tools to build their MVP exposed their AWS credentials in their GitHub repository, resulting in over $20,000 in unexpected cloud computing charges when malicious actors discovered and exploited these credentials.
This article provides a comprehensive guide to preventing credential exposure in vibe-coded applications. We’ll examine common patterns of credential exposure across popular full-stack app builders, demonstrate secure alternatives, and provide practical techniques for managing secrets safely. By implementing these practices, you can enjoy the productivity benefits of vibe coding without compromising on security.
Common Patterns of Credential Exposure in AI-Generated Code
AI coding assistants typically follow certain patterns when generating code that involves credentials. Understanding these patterns is the first step toward preventing credential exposure.
Hardcoded API Keys and Tokens
The most common pattern is the direct embedding of API keys, access tokens, and other credentials directly in source code:
// Example of hardcoded credentials in AI-generated code
const stripe = require('stripe')('sk_test_51HG8uEKFbX3LAgPOzCWKlNkJqIskqvQFyuiLX0b9aXXXXXXXXXXXX');
const firebaseConfig = {
apiKey: "AIzaSyBXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
authDomain: "example-app.firebaseapp.com",
projectId: "example-app",
storageBucket: "example-app.appspot.com",
messagingSenderId: "123456789012",
appId: "1:123456789012:web:a1b2c3d4e5f6g7h8i9j0k1"
};
This approach creates several security risks:
- Credentials are visible to anyone with access to the codebase
- Secrets persist in version control history indefinitely
- Different environments (development, staging, production) require different credentials
- Rotating credentials requires code changes
Frontend Exposure of Backend Secrets
AI tools often generate code that inadvertently exposes backend secrets in frontend code:
// React component with exposed API key
function WeatherWidget() {
const [weather, setWeather] = useState(null);
useEffect(() => {
fetch(`https://api.weatherapi.com/v1/current.json?key=a1b2c3d4e5f6g7h8i9j0k1&q=London`)
.then(response => response.json())
.then(data => setWeather(data));
}, []);
return (
<div className="weather-widget">
{weather && <p>Current temperature: {weather.current.temp_c}°C</p>}
</div>
);
}
This is particularly dangerous because:
- Frontend code is accessible to any user through browser developer tools
- API keys can be extracted and abused by malicious users
- Rate limits and usage quotas can be quickly exhausted
- Financial implications if using paid APIs
Insecure Database Connection Strings
AI-generated database connection code frequently includes credentials:
// Database connection with embedded credentials
const { Pool } = require('pg');
const pool = new Pool({
user: 'dbadmin',
host: 'database.example.com',
database: 'myapp',
password: 'SuperSecretPassword123!',
port: 5432,
});
This pattern is problematic because:
- Database credentials provide direct access to your data
- Compromised credentials can lead to data breaches
- Credentials are difficult to rotate without code changes
- Different environments require different connection strings
Commented Credentials
Sometimes AI tools include credentials in comments, which are still accessible in the codebase:
// API key for production: sk_live_51HG8uEKFbX3LAgPOzCWKlNkJqIskqvQFyuiLX0b9aXXXXXXXXXXXX
// Use this for testing: sk_test_51HG8uEKFbX3LAgPOzCWKlNkJqIskqvQFyuiLX0b9aXXXXXXXXXXXX
const stripe = require('stripe')(process.env.STRIPE_API_KEY);
Even though the code itself might use environment variables, the commented credentials are still exposed.
Platform-Specific Examples of Insecure Credential Handling
Each full-stack app builder has its own patterns of credential exposure. Let’s examine specific examples from each platform.
Lovable.dev
Lovable.dev integrates with Supabase for authentication and database operations. When prompted to create database functionality, it often generates code like this:
// Lovable.dev generated code for Supabase integration
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'https://xyzcompany.supabase.co';
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhtdGt2YnB1aGF5Y3Bram9tZHh4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTQ3MDI0MDUsImV4cCI6MjAxMDI3ODQwNX0.XXXXXXXXXXXXXXXXXXXXXXXXXX';
export const supabase = createClient(supabaseUrl, supabaseKey);
The issue here is that Lovable.dev often places this code in client-side files, exposing the Supabase anon key directly to users. While the anon key has limited permissions, it’s still not a best practice to expose it directly in frontend code.
Bolt.new
Bolt.new, with its focus on visual development and Figma integration, frequently generates code that includes API keys for various services:
// Bolt.new generated code for weather API integration
import React, { useEffect, useState } from 'react';
interface WeatherData {
temperature: number;
description: string;
icon: string;
}
export const WeatherWidget: React.FC = () => {
const [weather, setWeather] = useState<WeatherData | null>(null);
useEffect(() => {
const fetchWeather = async () => {
const response = await fetch(
'https://api.openweathermap.org/data/2.5/weather?q=London&appid=a1b2c3d4e5f6g7h8i9j0k1&units=metric'
);
const data = await response.json();
setWeather({
temperature: data.main.temp,
description: data.weather[0].description,
icon: data.weather[0].icon
});
};
fetchWeather();
}, []);
if (!weather) return <div>Loading weather...</div>;
return (
<div className="weather-widget">
<img
src={`http://openweathermap.org/img/wn/${weather.icon}@2x.png`}
alt={weather.description}
/>
<p>{weather.temperature}°C</p>
<p>{weather.description}</p>
</div>
);
};
This code exposes the OpenWeatherMap API key directly in the frontend, making it vulnerable to abuse.
Tempo Labs
Tempo Labs, with its focus on comprehensive application development including payment processing, often generates code with payment API credentials:
// Tempo Labs generated code for Stripe integration
import { loadStripe } from '@stripe/stripe-js';
// Initialize Stripe with publishable key
const stripePromise = loadStripe('pk_test_51HG8uEKFbX3LAgPOzCWKlNkJqIskqvQFyuiLX0b9aXXXXXXXXXXXX');
// Server-side code in the same file
const stripe = require('stripe')('sk_test_51HG8uEKFbX3LAgPOzCWKlNkJqIskqvQFyuiLX0b9aXXXXXXXXXXXX');
export async function createPaymentIntent(amount) {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
});
return paymentIntent;
}
The issue here is that Tempo Labs sometimes mixes client and server code, potentially exposing the secret key in client-accessible files.
Base44
Base44, being more bare-bones and developer-focused, often generates code with database connection strings:
// Base44 generated code for MongoDB connection
const mongoose = require('mongoose');
// Connect to MongoDB
mongoose.connect('mongodb+srv://admin:SecretPassword123@cluster0.mongodb.net/myapp?retryWrites=true&w=majority', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
This directly exposes the MongoDB username and password in the source code.
Replit
Replit’s AI assistant often generates code with hardcoded credentials, particularly in Python applications:
# Replit generated code for AWS S3 integration
import boto3
# Create an S3 client
s3 = boto3.client(
's3',
aws_access_key_id='AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
region_name='us-west-2'
)
def upload_file(file_name, bucket, object_name=None):
"""Upload a file to an S3 bucket"""
if object_name is None:
object_name = file_name
try:
s3.upload_file(file_name, bucket, object_name)
return True
except Exception as e:
print(f"Error uploading file: {e}")
return False
This code exposes AWS credentials directly, which could lead to unauthorized access to AWS services and potential financial impact.
Secure Alternatives for Credential Management
Now that we’ve identified the common patterns of credential exposure, let’s explore secure alternatives for managing credentials in vibe-coded applications.
Environment Variables
The most widely used approach for credential management is environment variables:
// Using environment variables for credentials
const stripe = require('stripe')(process.env.STRIPE_API_KEY);
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID
};
Benefits of this approach:
- Credentials are kept out of the codebase
- Different environments can have different credential values
- Credential rotation doesn’t require code changes
- Standard practice across the industry
Implementation varies by platform:
Lovable.dev Environment Variables
In Lovable.dev, you can use environment variables by creating a .env
file in your project root and then using the process.env
object:
// Lovable.dev secure Supabase integration
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
For client-side code, prefix variables with NEXT_PUBLIC_
to make them available in the browser, but remember that these values will still be visible to users. For truly sensitive credentials, use server-side code.
Bolt.new Environment Variables
Bolt.new supports environment variables through a similar .env
file approach:
// Bolt.new secure weather API integration
import React, { useEffect, useState } from 'react';
interface WeatherData {
temperature: number;
description: string;
icon: string;
}
export const WeatherWidget: React.FC = () => {
const [weather, setWeather] = useState<WeatherData | null>(null);
useEffect(() => {
const fetchWeather = async () => {
// Make API call to your backend instead of directly to the weather API
const response = await fetch('/api/weather?city=London');
const data = await response.json();
setWeather({
temperature: data.main.temp,
description: data.weather[0].description,
icon: data.weather[0].icon
});
};
fetchWeather();
}, []);
if (!weather) return <div>Loading weather...</div>;
return (
<div className="weather-widget">
<img
src={`http://openweathermap.org/img/wn/${weather.icon}@2x.png`}
alt={weather.description}
/>
<p>{weather.temperature}°C</p>
<p>{weather.description}</p>
</div>
);
};
// In a separate server-side file (e.g., /api/weather.js)
export default async function handler(req, res) {
const { city } = req.query;
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.OPENWEATHERMAP_API_KEY}&units=metric`
);
const data = await response.json();
res.status(200).json(data);
}
This approach keeps the API key on the server side, preventing exposure to clients.
Tempo Labs Environment Variables
Tempo Labs supports environment variables and proper separation of client and server code:
// Tempo Labs secure Stripe integration
// Client-side code
import { loadStripe } from '@stripe/stripe-js';
// Only use the publishable key on the client side
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
export function CheckoutForm({ clientSecret }) {
// Implementation using clientSecret from server
}
// In a separate server-side file
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function createPaymentIntent(amount) {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
});
return { clientSecret: paymentIntent.client_secret };
}
This properly separates the publishable key (which is safe to use on the client) from the secret key (which must be kept on the server).
Base44 Environment Variables
Base44 supports environment variables for database connections:
// Base44 secure MongoDB connection
const mongoose = require('mongoose');
require('dotenv').config();
// Connect to MongoDB using environment variables
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
This keeps the connection string with credentials out of the codebase.
Replit Environment Variables
Replit provides a built-in secrets management system accessible through the Replit interface:
# Replit secure AWS S3 integration
import boto3
import os
# Create an S3 client using environment variables
s3 = boto3.client(
's3',
aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'],
aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'],
region_name=os.environ['AWS_REGION']
)
def upload_file(file_name, bucket, object_name=None):
"""Upload a file to an S3 bucket"""
if object_name is None:
object_name = file_name
try:
s3.upload_file(file_name, bucket, object_name)
return True
except Exception as e:
print(f"Error uploading file: {e}")
return False
In Replit, you can set these environment variables in the “Secrets” tab of your project.
Backend API Proxies
For frontend applications that need to access third-party APIs, a backend proxy is the most secure approach:
// Frontend code
async function getWeatherData(city) {
// Call your own backend API instead of the third-party API directly
const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
return response.json();
}
// Backend code (separate file)
import express from 'express';
const router = express.Router();
router.get('/weather', async (req, res) => {
const { city } = req.query;
// Make the actual API call with your secret key
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${city}`
);
const data = await response.json();
res.json(data);
});
export default router;
This approach:
- Keeps API keys completely server-side
- Allows for additional validation and rate limiting
- Provides a consistent API for your frontend regardless of the third-party service
- Enables easier switching of providers if needed
Dedicated Secrets Management Solutions
For more advanced applications, consider dedicated secrets management solutions:
AWS Secrets Manager
// Using AWS Secrets Manager
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
async function getDatabaseCredentials() {
const data = await secretsManager.getSecretValue({ SecretId: 'database/credentials' }).promise();
const secret = JSON.parse(data.SecretString);
return {
host: secret.host,
username: secret.username,
password: secret.password,
database: secret.dbname
};
}
async function connectToDatabase() {
const credentials = await getDatabaseCredentials();
// Use credentials to connect to the database
}
HashiCorp Vault
// Using HashiCorp Vault
const vault = require('node-vault')({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
async function getApiKey(service) {
const result = await vault.read(`secret/api-keys/${service}`);
return result.data.key;
}
async function callExternalApi() {
const apiKey = await getApiKey('stripe');
// Use apiKey to make API calls
}
Infisical
// Using Infisical for secrets management
const { Infisical } = require('infisical-node');
const infisical = new Infisical({
token: process.env.INFISICAL_TOKEN,
});
async function getSecrets() {
const secrets = await infisical.getAllSecrets({
environment: process.env.NODE_ENV,
projectId: process.env.INFISICAL_PROJECT_ID,
});
return secrets;
}
async function initializeApp() {
const secrets = await getSecrets();
// Use secrets.STRIPE_API_KEY, secrets.DATABASE_URL, etc.
}
These solutions provide:
- Centralized secrets management
- Automatic rotation capabilities
- Access controls and audit logging
- Integration with identity providers
Implementing Environment Variables in Each Platform
Let’s look at the specific steps to implement environment variables in each full-stack app builder.
Lovable.dev
-
Create a
.env.local
file in your project root:NEXT_PUBLIC_SUPABASE_URL=https://xyzcompany.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
-
For server-side operations, create API routes that use the service key:
// pages/api/protected-data.js import { createClient } from '@supabase/supabase-js'; const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY ); export default async function handler(req, res) { // Use supabase with service key for admin operations const { data, error } = await supabase .from('protected_table') .select('*'); if (error) return res.status(500).json({ error: error.message }); return res.status(200).json(data); }
-
For client-side operations, use only the anon key:
// lib/supabaseClient.js import { createClient } from '@supabase/supabase-js'; export const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY );
Bolt.new
-
Create a
.env
file in your project root:OPENWEATHERMAP_API_KEY=a1b2c3d4e5f6g7h8i9j0k1 DATABASE_URL=postgresql://username:password@localhost:5432/mydb
-
Install the dotenv package:
npm install dotenv
-
Load environment variables in your server-side code:
// server.js require('dotenv').config(); const express = require('express'); const app = express(); app.get('/api/weather', async (req, res) => { const { city } = req.query; const response = await fetch( `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.OPENWEATHERMAP_API_KEY}&units=metric` ); const data = await response.json(); res.json(data); }); app.listen(3000, () => { console.log('Server running on port 3000'); });
Tempo Labs
-
Create a
.env.local
file in your project root:STRIPE_PUBLISHABLE_KEY=pk_test_51HG8uEKFbX3LAgPO... STRIPE_SECRET_KEY=sk_test_51HG8uEKFbX3LAgPO...
-
Create separate files for client and server code:
// lib/stripe-client.js (client-side) import { loadStripe } from '@stripe/stripe-js'; let stripePromise; export const getStripe = () => { if (!stripePromise) { stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); } return stripePromise; };
// pages/api/create-payment-intent.js (server-side) import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).end('Method Not Allowed'); } try { const { amount } = req.body; const paymentIntent = await stripe.paymentIntents.create({ amount, currency: 'usd', }); res.status(200).json({ clientSecret: paymentIntent.client_secret }); } catch (error) { res.status(500).json({ error: error.message }); } }
Base44
-
Create a
.env
file in your project root:MONGODB_URI=mongodb+srv://admin:SecretPassword123@cluster0.mongodb.net/myapp?retryWrites=true&w=majority JWT_SECRET=your_jwt_secret_key_here
-
Install the dotenv package:
npm install dotenv
-
Load environment variables at the entry point of your application:
// app.js require('dotenv').config(); const express = require('express'); const mongoose = require('mongoose'); const app = express(); mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true }) .then(() => console.log('Connected to MongoDB')) .catch(err => console.error('MongoDB connection error:', err)); // Rest of your application code
Replit
-
In Replit, click on the padlock icon in the sidebar or navigate to the “Secrets” tab.
-
Add your environment variables:
- Key:
AWS_ACCESS_KEY_ID
, Value:AKIAIOSFODNN7EXAMPLE
- Key:
AWS_SECRET_ACCESS_KEY
, Value:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
- Key:
AWS_REGION
, Value:us-west-2
- Key:
-
Access these variables in your code:
# main.py import os import boto3 # Create an S3 client using environment variables s3 = boto3.client( 's3', aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'], aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'], region_name=os.environ['AWS_REGION'] ) # Rest of your application code
Detection and Remediation of Exposed Credentials
Even with best practices in place, credentials can sometimes be accidentally exposed. Here’s how to detect and remediate such issues.
Detection Tools
Several tools can help detect exposed credentials in your codebase:
GitGuardian
GitGuardian scans your repositories for exposed secrets and alerts you when they’re found:
# Install GitGuardian CLI
pip install ggshield
# Scan a directory
ggshield scan path /path/to/your/code
# Scan staged changes
ggshield scan pre-commit
TruffleHog
TruffleHog searches through git repositories for secrets:
# Install TruffleHog
pip install truffleHog
# Scan a repository
trufflehog --regex --entropy=True https://github.com/your-username/your-repo.git
detect-secrets
Yelp’s detect-secrets tool can be integrated into your CI/CD pipeline:
# Install detect-secrets
pip install detect-secrets
# Scan a directory
detect-secrets scan /path/to/your/code
# Add a pre-commit hook
detect-secrets scan > .secrets.baseline
Remediation Steps
If you discover exposed credentials, follow these steps to remediate the issue:
-
Revoke and rotate the credentials immediately:
- For API keys, regenerate them in the service provider’s dashboard
- For database credentials, change the passwords
- For JWT secrets, generate new secrets and update your authentication system
-
Remove credentials from git history:
# Use BFG Repo-Cleaner to remove sensitive data java -jar bfg.jar --replace-text credentials.txt your-repo.git # Or use git filter-branch git filter-branch --force --index-filter \ "git rm --cached --ignore-unmatch path/to/file/with/credentials.js" \ --prune-empty --tag-name-filter cat -- --all
-
Force push the changes:
git push origin --force --all
-
Implement proper credential management:
- Move credentials to environment variables
- Update your code to use the environment variables
- Add sensitive files to
.gitignore
-
Audit for potential breaches:
- Check access logs for suspicious activity
- Monitor for unauthorized API usage
- Review database access patterns
Best Practices for Secure Credential Management
To prevent credential exposure in vibe-coded applications, follow these best practices:
1. Use Specific Prompts for Security
When using AI tools to generate code, include security requirements in your prompts:
Instead of:
Create a function to connect to the database
Use:
Create a function to connect to the database using environment variables for credentials, following security best practices. Do not hardcode any credentials.
2. Implement a Secrets Management Strategy
Choose a secrets management approach based on your project’s complexity:
- For simple projects: Environment variables with
.env
files - For team projects: Centralized secrets management (AWS Secrets Manager, HashiCorp Vault, Infisical)
- For enterprise applications: Integrated secrets management with rotation and audit capabilities
3. Separate Frontend and Backend Concerns
Never expose sensitive credentials in frontend code:
- Create backend API endpoints that proxy requests to third-party services
- Use environment variables with different prefixes for client-side and server-side variables
- Implement proper authentication for your backend APIs
4. Implement Least Privilege Access
Apply the principle of least privilege to all credentials:
- Create service-specific API keys with minimal permissions
- Use read-only database users for query operations
- Create separate credentials for different services and environments
5. Automate Secret Detection
Integrate secret detection tools into your workflow:
- Add pre-commit hooks to prevent committing secrets
- Include secret scanning in CI/CD pipelines
- Set up automated alerts for potential credential exposure
6. Plan for Credential Rotation
Design your application to support easy credential rotation:
- Use environment variables or secrets management solutions
- Implement graceful handling of credential changes
- Document the rotation process for all credentials
7. Educate Your Team
Ensure everyone involved in the project understands credential security:
- Share best practices for vibe coding securely
- Establish clear guidelines for credential management
- Create a response plan for credential exposure incidents
Real-World Scenarios and Solutions
Let’s examine some real-world scenarios of credential exposure in vibe-coded applications and their solutions.
Scenario 1: Exposed API Key in Frontend Code
Problem: A developer used vibe coding to create a weather dashboard that directly calls the OpenWeatherMap API from React components, exposing the API key.
Solution:
-
Create a backend API endpoint to proxy the weather API requests:
// pages/api/weather.js export default async function handler(req, res) { const { city } = req.query; const response = await fetch( `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.OPENWEATHERMAP_API_KEY}&units=metric` ); const data = await response.json(); res.status(200).json(data); }
-
Update the frontend code to call your backend API instead:
// components/WeatherWidget.js useEffect(() => { const fetchWeather = async () => { const response = await fetch(`/api/weather?city=London`); const data = await response.json(); setWeather(data); }; fetchWeather(); }, []);
Scenario 2: Database Credentials in Version Control
Problem: A team used Base44 to generate a Node.js application with MongoDB integration, and the database connection string with credentials was committed to the repository.
Solution:
-
Rotate the database credentials immediately.
-
Move the connection string to an environment variable:
// database.js const mongoose = require('mongoose'); require('dotenv').config(); mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
-
Add
.env
to.gitignore
to prevent future exposure:# .gitignore .env .env.local .env.development .env.test .env.production
-
Use BFG Repo-Cleaner to remove the credentials from git history:
java -jar bfg.jar --replace-text credentials.txt your-repo.git
Scenario 3: Mixed Client and Server Code with Sensitive Keys
Problem: A developer used Tempo Labs to create a payment processing system, but the code mixed client and server concerns, potentially exposing the Stripe secret key.
Solution:
-
Separate client and server code into different files:
// lib/stripe-client.js (client-side) import { loadStripe } from '@stripe/stripe-js'; export const getStripe = () => { return loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); };
// pages/api/create-payment.js (server-side) import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export default async function handler(req, res) { // Server-side payment processing logic }
-
Use environment variables with appropriate prefixes:
# .env.local NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_SECRET_KEY=sk_test_...
-
Implement proper API endpoints for payment operations that require the secret key.
Conclusion
Credential exposure is one of the most critical security vulnerabilities in vibe-coded applications. The convenience and speed of AI-assisted development can sometimes lead to insecure practices, particularly when it comes to handling sensitive credentials and API keys.
By understanding the common patterns of credential exposure across popular full-stack app builders like Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit, you can proactively implement secure alternatives. Environment variables, backend API proxies, and dedicated secrets management solutions provide robust approaches to keeping your credentials safe.
Remember to:
- Use specific prompts that include security requirements
- Implement a comprehensive secrets management strategy
- Separate frontend and backend concerns
- Apply the principle of least privilege
- Automate secret detection
- Plan for credential rotation
- Educate your team on secure practices
With these practices in place, you can enjoy the productivity benefits of vibe coding without compromising on security. Your applications will be both rapidly developed and robustly protected against one of the most common security vulnerabilities.
Additional Resources
- OWASP Secrets Management Guide
- GitGuardian Documentation
- AWS Secrets Manager Documentation
- HashiCorp Vault Documentation
- Infisical Documentation
- dotenv Documentation
References
- GitGuardian. (2024). “State of Secrets Sprawl 2024.” Retrieved from https://www.gitguardian.com/state-of-secrets-sprawl-report-2024
- OWASP. (2025). “Top 10 API Security Risks.” Retrieved from https://owasp.org/API-Security/editions/2025/en/0x00-introduction/
- Checkmarx. (2025). “Security in Vibe Coding: Innovation Meets Risk.” Retrieved from https://checkmarx.com/blog/security-in-vibe-coding/
- Segura, T. (2025). “A Vibe Coding Security Playbook.” Infisical. Retrieved from https://infisical.com/blog/vibe-coding-security-playbook
- Trotta, F. (2025). “5 Vibe Coding Risks and Ways to Avoid Them in 2025.” Zencoder. Retrieved from https://zencoder.ai/blog/vibe-coding-risks