Vibe Coding to Production: The 6 Steps You're Probably Missing
Your app works perfectly on localhost:3000. Authentication flows smoothly. The database queries return data instantly. Everything just… works.
Then you deploy to production and it all falls apart.
Login fails with a mysterious 401 error. The database times out. API calls that took milliseconds now take 10 seconds. Users report that the app “feels broken.” You’re frantically refreshing logs, searching StackOverflow at 2 AM, and wondering what went wrong.
Welcome to the production reality check that hits every vibe coder.
Why “Works on My Machine” Isn’t Enough
AI coding tools optimize for one thing: getting code running right now. They don’t think about:
- What happens when 100 users hit your server simultaneously
- How your app behaves when the database is on a different server (with network latency)
- Whether your environment variables are properly configured for production
- If your authentication tokens work across different domains
- How users experience errors when things go wrong
According to research from Convex in 2025, deployment failures are the #1 reason vibe-coded apps never launch. The prototype works. The vision is clear. But the gap between localhost and production kills the project.
Let’s bridge that gap with a systematic approach.
The 6 Production Readiness Steps
Step 1: Environment Configuration (The Foundation)
The Problem: AI-generated code often hardcodes values or uses development-only settings. Your local database URL, API keys, and secret tokens are sitting right in your code.
The Solution:
Create proper environment files:
# .env.development (local)
DATABASE_URL="postgresql://localhost:5432/myapp_dev"
API_KEY="dev_test_key_12345"
JWT_SECRET="local-dev-secret"
REDIS_URL="redis://localhost:6379"
NODE_ENV="development"
# .env.production (server)
DATABASE_URL="postgresql://prod-db.server.com:5432/myapp_prod"
API_KEY="prod_live_key_xxxxx"
JWT_SECRET="<generated-secure-random-string>"
REDIS_URL="rediss://prod-redis.server.com:6380"
NODE_ENV="production"
Critical environment variables you need:
- Database connection URLs (with credentials)
- Third-party API keys (Stripe, SendGrid, etc.)
- JWT/session secrets (use a secure random generator)
- CORS allowed origins
- Frontend URL (for OAuth callbacks)
- Log levels and destinations
- Feature flags
Security rules:
- ✅ NEVER commit
.envfiles to git - ✅ Add
.env*to.gitignore - ✅ Use different values for dev/staging/production
- ✅ Rotate secrets periodically (at least every 90 days)
- ✅ Store production secrets in your hosting platform’s secure storage
Quick Test:
# Search your codebase for hardcoded secrets
grep -r "sk_live_" src/ # Stripe keys
grep -r "api.openai.com" src/ # API URLs
grep -r "password" src/ # Database credentials
If you find anything, move it to environment variables immediately.
Step 2: Authentication & Session Management
The Problem: Auth that works locally breaks in production because of cookie domains, CORS, HTTPS requirements, or token expiration issues.
The Solution:
JWT Token Configuration:
// ❌ What AI often generates
const token = jwt.sign({ userId: user.id }, 'secret123');
// ✅ What production needs
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET, // From environment variable
{
expiresIn: '7d', // Tokens expire
issuer: 'myapp.com', // Verify token origin
audience: 'myapp-users' // Intended recipient
}
);
Session Cookie Settings:
// ❌ Development default
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}));
// ✅ Production ready
app.use(session({
secret: process.env.SESSION_SECRET,
name: 'sessionId', // Don't use default 'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
httpOnly: true, // No JavaScript access
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
sameSite: 'lax', // CSRF protection
domain: process.env.COOKIE_DOMAIN // Set explicitly
},
store: new RedisStore({ // Don't use memory store
client: redisClient
})
}));
CORS Configuration:
// ❌ AI-generated "make it work" solution
app.use(cors({ origin: '*' })); // Allows anyone!
// ✅ Production security
app.use(cors({
origin: process.env.FRONTEND_URL, // Specific domain
credentials: true, // Allow cookies
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
OAuth Callback URLs: When using OAuth (Google, GitHub, etc.), your callback URLs must match exactly:
- Development:
http://localhost:3000/auth/callback - Production:
https://yourdomain.com/auth/callback
Update these in your OAuth provider’s dashboard before deploying.
Step 3: Database Production Configuration
The Problem: Your local SQLite/PostgreSQL works great with zero configuration. Production databases need connection pooling, timeouts, SSL, and proper error handling.
The Solution:
Connection Pooling (Essential for Performance):
// ❌ What breaks under load
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
// ✅ Production ready
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: false // For Heroku, Render, etc.
} : false,
max: 20, // Maximum connections
min: 5, // Minimum idle connections
idleTimeoutMillis: 30000, // Close idle after 30s
connectionTimeoutMillis: 2000 // Fail fast
});
// Handle errors gracefully
pool.on('error', (err, client) => {
console.error('Unexpected database error:', err);
// Don't crash the app
});
Query Timeouts:
// Add timeout to every query
const query = {
text: 'SELECT * FROM users WHERE email = $1',
values: [email],
timeout: 5000 // 5 second timeout
};
try {
const result = await pool.query(query);
return result.rows[0];
} catch (err) {
if (err.message.includes('timeout')) {
throw new Error('Database query timeout - try again');
}
throw err;
}
Migrations:
# Use a migration tool (never manually edit production DB)
npm install db-migrate db-migrate-pg
# Create migration
db-migrate create add-users-table
# Apply to production
db-migrate up --env production
Database Backups:
- Daily automated backups (minimum)
- Test restore process monthly
- Keep at least 30 days of backups
- Store backups in different region than production
Step 4: API Resilience & Error Handling
The Problem: Third-party APIs fail. Networks hiccup. Timeouts happen. AI-generated code rarely handles any of this gracefully.
The Solution:
Retry Logic with Exponential Backoff:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const timeout = new AbortController();
const timeoutId = setTimeout(() => timeout.abort(), 10000);
const response = await fetch(url, {
...options,
signal: timeout.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
console.warn(`Attempt ${i + 1} failed:`, error.message);
if (i < maxRetries - 1) {
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
Circuit Breaker Pattern:
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failures = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN - service unavailable');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
// Usage
const stripeBreaker = new CircuitBreaker();
app.post('/api/charge', async (req, res) => {
try {
const charge = await stripeBreaker.execute(() =>
stripe.charges.create(req.body)
);
res.json({ success: true, charge });
} catch (error) {
res.status(503).json({
error: 'Payment service temporarily unavailable'
});
}
});
Rate Limiting:
import rateLimit from 'express-rate-limit';
// Prevent brute force attacks
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, try again later',
standardHeaders: true,
legacyHeaders: false
});
app.post('/api/login', loginLimiter, async (req, res) => {
// Login logic
});
// General API rate limiting
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Too many requests, slow down'
});
app.use('/api/', apiLimiter);
Step 5: Monitoring & Observability
The Problem: When your app breaks in production, you need to know immediately—and have enough information to fix it fast.
The Solution:
Health Check Endpoint:
app.get('/health', async (req, res) => {
const checks = {
uptime: process.uptime(),
timestamp: Date.now(),
database: 'unknown',
redis: 'unknown'
};
// Check database
try {
await pool.query('SELECT 1');
checks.database = 'healthy';
} catch (err) {
checks.database = 'unhealthy';
}
// Check Redis
try {
await redis.ping();
checks.redis = 'healthy';
} catch (err) {
checks.redis = 'unhealthy';
}
const isHealthy = checks.database === 'healthy' && checks.redis === 'healthy';
res.status(isHealthy ? 200 : 503).json(checks);
});
Structured Logging:
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// In production, also log to a service
if (process.env.NODE_ENV === 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// Usage
logger.info('User logged in', { userId: user.id, email: user.email });
logger.error('Payment failed', { error: err.message, userId: user.id });
Error Tracking (Use a Service):
Free options:
- Sentry (10,000 errors/month free)
- Rollbar (5,000 events/month free)
- Bugsnag (7,500 events/month free)
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1 // 10% of transactions
});
// Automatically capture errors
app.use(Sentry.Handlers.errorHandler());
Performance Monitoring:
// Track slow queries
pool.on('acquire', () => {
const start = Date.now();
return () => {
const duration = Date.now() - start;
if (duration > 1000) {
logger.warn('Slow query detected', { duration });
}
};
});
// Track API response times
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('Request completed', {
method: req.method,
path: req.path,
status: res.statusCode,
duration
});
});
next();
});
Step 6: Graceful Degradation & Rollback Plan
The Problem: Things will break. Have a plan for when they do.
The Solution:
Graceful Shutdown:
let isShuttingDown = false;
async function shutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`${signal} received, shutting down gracefully`);
// Stop accepting new requests
server.close(() => {
console.log('HTTP server closed');
});
// Close database connections
try {
await pool.end();
console.log('Database connections closed');
} catch (err) {
console.error('Error closing database:', err);
}
// Close Redis connection
try {
await redis.quit();
console.log('Redis connection closed');
} catch (err) {
console.error('Error closing Redis:', err);
}
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
Feature Flags:
// Simple feature flag system
const features = {
newCheckout: process.env.FEATURE_NEW_CHECKOUT === 'true',
aiAssistant: process.env.FEATURE_AI_ASSISTANT === 'true'
};
// Use in code
if (features.newCheckout) {
return renderNewCheckout();
} else {
return renderOldCheckout(); // Fallback to working version
}
Blue-Green Deployment:
- Deploy new version to separate environment
- Test thoroughly with real data
- Switch traffic gradually (10% → 50% → 100%)
- Keep old version running for quick rollback
Rollback Checklist:
## If Deployment Fails:
1. [ ] Check error logs in monitoring tool
2. [ ] Verify environment variables are set
3. [ ] Test database connection manually
4. [ ] Check if health endpoint responds
5. [ ] If critical issue: rollback to previous version
6. [ ] If minor issue: apply hotfix
7. [ ] Document what went wrong for next time
Production Launch Checklist
Before you hit “deploy,” verify these items:
Pre-Launch (Day Before):
- All environment variables configured in hosting platform
- Database backups are automated and tested
- Health check endpoint returns 200 OK
- Error tracking service (Sentry) is configured
- SSL certificate is valid and auto-renewing
- CORS settings allow your frontend domain
- Rate limiting is active on sensitive endpoints
- Monitoring dashboards are set up
Launch Day:
- Deploy to production during low-traffic hours
- Monitor error rates for first 30 minutes
- Test critical user flows manually (signup, login, core feature)
- Check that emails are being sent (if applicable)
- Verify payment processing works (test mode first!)
- Monitor server CPU and memory usage
- Have rollback plan ready to execute
Post-Launch (First Week):
- Review error logs daily
- Monitor database performance
- Check for slow API endpoints
- Gather user feedback on bugs
- Set up alerts for high error rates
- Document common issues and fixes
Common Deployment Failures & Fixes
Failure 1: “Module not found” Error
Cause: Dependencies missing from package.json
Fix: npm install --save <missing-package> and commit
Failure 2: Database Connection Timeout
Cause: Firewall or incorrect connection string Fix: Verify database URL, check IP whitelist, test connection from server
Failure 3: CORS Errors in Production
Cause: Frontend origin not in allowed list Fix: Add exact frontend URL to CORS config (including https://)
Failure 4: Environment Variables Not Found
Cause: Not set in hosting platform Fix: Double-check spelling, restart app after setting them
Failure 5: Static Files 404
Cause: Build step not run or wrong directory Fix: Verify build script runs, check output directory path
When to Get Professional DevOps Help
Bring in an expert if:
- Deployment takes more than 3 failed attempts
- Database migrations are breaking production data
- You’re experiencing downtime or data loss
- You need high availability (99.9%+ uptime)
- You’re handling payments or sensitive data
- You need to scale beyond 1,000 concurrent users
Cost of staying stuck:
- Lost launch momentum
- Customer trust eroded by bugs
- Revenue delay (every day costs money)
- Your time ($50/hr × 40 hours = $2,000+)
Investment in DevOps help:
- Production deployment setup: $800-1,500
- Full CI/CD pipeline: $1,500-3,000
- Monitoring and scaling: $1,000-2,000
Real Success Story
Marcus’s SaaS Journey:
- Week 1-4: Built job board app with Bolt.new and Claude
- Week 5: Deploy attempt #1 - auth failed, gave up
- Week 6: Deploy attempt #2 - database errors, rolled back
- Week 7: Hired DevOps consultant for $1,200
- Set up proper environment config
- Implemented monitoring and backups
- Created deployment checklist
- Week 8: Successful launch, 50 paying customers in month 1
- Month 6: $4,800 MRR, rock-solid infrastructure
Marcus’s Takeaway: “I wasted 2 weeks trying to figure it out alone. $1,200 and 8 hours with an expert saved me months of frustration.”
Your Production Readiness Action Plan
This Week: Foundation
- ✅ Move all secrets to environment variables
- ✅ Fix authentication for production domains
- ✅ Set up database connection pooling
- ✅ Add retry logic to critical API calls
Next Week: Monitoring
- ✅ Implement health check endpoint
- ✅ Set up error tracking (Sentry)
- ✅ Add structured logging
- ✅ Create monitoring dashboard
Launch Week: Deployment
- ✅ Run through production checklist
- ✅ Deploy to staging first
- ✅ Test critical user flows
- ✅ Deploy to production during low traffic
- ✅ Monitor closely for 24 hours
The Bottom Line
“It works on my machine” is where excitement lives. Production is where reality lives.
The gap between them isn’t about fancy architecture or enterprise-scale infrastructure. It’s about doing the unsexy fundamentals that AI tools skip:
- Proper configuration
- Graceful error handling
- Connection pooling
- Monitoring and logging
- Rollback plans
You don’t need a PhD in distributed systems. You just need to systematically work through these 6 steps. Most vibe-coded apps can go from localhost to production in 1-2 weeks with this checklist.
Your prototype proves the idea works. Production proves you can deliver.
Need Production Deployment Support?
GTM Enterprises specializes in taking AI-generated apps from localhost to production-ready infrastructure. We've successfully deployed hundreds of projects.
Our Production Deployment package includes:
- Environment configuration and secrets management
- Authentication and CORS setup
- Database optimization and backups
- Monitoring, logging, and alerting
- Launch day support and troubleshooting