
Migrating Your Replit App to Production: A Complete Guide
Migrating Your Replit App to Production: A Complete Guide
Replit is perfect for rapid prototyping. But when your app is ready for real users, you need real infrastructure. Here's exactly how to make that transition.
Why Migrate from Replit?
Replit is fantastic for:
- Rapid prototyping
- Learning and experimentation
- Hackathons and demos
- Small personal projects
But production apps need:
- Higher performance - Dedicated resources, not shared servers
- Better security - SOC 2 compliance, encryption, audit logs
- Scalability - Handle traffic spikes without crashing
- Custom infrastructure - Networking rules, monitoring, backups
- Lower costs - At scale, Replit's pricing becomes expensive
Pre-Migration Checklist
Before you start, gather:
- Your complete Replit codebase
- Environment variables and secrets
- Database schema and data
- List of all dependencies
- API keys and third-party integrations
- Current user data (if applicable)
- Documentation of features and workflows
Phase 1: Choose Your New Home
Option A: Vercel (Best for Web Apps)
Perfect for:
- Next.js, React, Vue applications
- Static sites with serverless functions
- Fast deployment requirements
Pros:
- Zero configuration for Next.js
- Excellent performance (global CDN)
- Generous free tier
- Git-based deployments
Cons:
- Serverless limitations (execution time, memory)
- Higher costs at enterprise scale
Pricing: Free → $20/month → Custom
Option B: Railway (Best for Full-Stack Apps)
Perfect for:
- Apps with persistent servers
- PostgreSQL/MySQL databases
- Microservices architectures
Pros:
- Simple deployment from Git
- Built-in database hosting
- Flexible pricing
- Great DX
Cons:
- Smaller than AWS/GCP
- Fewer advanced features
Pricing: $5/month → pay-per-use
Option C: AWS/GCP/Azure (Best for Enterprise)
Perfect for:
- Complex, large-scale applications
- Compliance requirements
- Custom networking needs
- Maximum control
Pros:
- Unlimited scalability
- Every feature imaginable
- Enterprise support
Cons:
- Steep learning curve
- Complex billing
- Requires DevOps knowledge
Pricing: Pay-per-use (can get expensive)
Option D: DigitalOcean/Linode (Best for Control + Simplicity)
Perfect for:
- Teams wanting VPS control
- Predictable pricing
- Medium-scale applications
Pros:
- Simple pricing ($5-$40/month per server)
- Full server control
- Good documentation
Cons:
- You manage infrastructure
- Manual scaling
Pricing: $5-$40/month per droplet
Phase 2: Prepare Your Code
Step 1: Set Up Version Control
If you haven't already:
# In your Replit shell
git init
git add .
git commit -m "Initial commit from Replit"
# Create GitHub repo and push
git remote add origin https://github.com/yourusername/your-repo.git
git push -u origin main
Step 2: Externalize Configuration
Replace hardcoded values with environment variables:
Before (❌ Bad):
const db = postgres('postgresql://user:password@localhost:5432/mydb')
const apiKey = 'sk-1234567890abcdef'
After (✅ Good):
const db = postgres(process.env.DATABASE_URL)
const apiKey = process.env.API_KEY
Create a .env.example file:
DATABASE_URL=postgresql://user:password@host:5432/dbname
API_KEY=your-api-key-here
STRIPE_SECRET=your-stripe-secret
Step 3: Update Dependencies
Check your package.json or requirements.txt:
# Node.js
npm audit fix
npm update
# Python
pip list --outdated
pip install --upgrade package-name
Step 4: Add Production Scripts
Update package.json:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"test": "jest"
}
}
Phase 2.5: Scaling Readiness Checklist
Before migrating, test if your app is ready for production scale. Most Replit apps work great for 5 users but break at 50-100 concurrent users.
Load Testing
Test your app with realistic traffic before migration:
# Install k6 load testing tool
brew install k6 # or download from k6.io
# Create simple load test
cat > load-test.js <<EOF
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 10 }, // Ramp up to 10 users
{ duration: '5m', target: 50 }, // Stay at 50 users
{ duration: '2m', target: 100 }, // Peak at 100 users
{ duration: '5m', target: 0 }, // Ramp down
],
};
export default function () {
const response = http.get('https://your-replit-url.repl.co');
check(response, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
EOF
# Run the test
k6 run load-test.js
Common Scaling Issues to Fix
1. Database Connection Pooling
Check: Does your app create a new database connection per request?
// ❌ BAD: New connection every request (breaks at ~25 users)
app.get('/users', async (req, res) => {
const db = new Database(process.env.DATABASE_URL)
const users = await db.query('SELECT * FROM users')
res.json(users)
})
// ✅ GOOD: Connection pool shared across requests
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Maximum connections
idleTimeoutMillis: 30000, // Close idle connections
connectionTimeoutMillis: 2000,
})
app.get('/users', async (req, res) => {
const users = await pool.query('SELECT * FROM users')
res.json(users)
})
2. N+1 Query Problem
Check: Do you loop through records making additional queries?
// ❌ BAD: N+1 queries (1 query + N queries, extremely slow with many records)
const users = await db.query('SELECT * FROM users')
for (const user of users) {
user.posts = await db.query('SELECT * FROM posts WHERE user_id = $1', [user.id])
}
// ✅ GOOD: Single query with JOIN
const users = await db.query(`
SELECT
users.*,
json_agg(
json_build_object('id', posts.id, 'title', posts.title)
) as posts
FROM users
LEFT JOIN posts ON posts.user_id = users.id
GROUP BY users.id
`)
3. Missing Indexes
Check: Do your database queries scan entire tables?
-- Check query performance
EXPLAIN ANALYZE SELECT * FROM posts WHERE user_id = 123;
-- If you see "Seq Scan" instead of "Index Scan", add indexes:
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
4. No Caching
Check: Do you query the database for the same data repeatedly?
// ❌ BAD: Database hit on every request
app.get('/config', async (req, res) => {
const config = await db.query('SELECT * FROM config WHERE id = 1')
res.json(config)
})
// ✅ GOOD: Cache frequently accessed data
const NodeCache = require('node-cache')
const cache = new NodeCache({ stdTTL: 600 }) // 10 minute TTL
app.get('/config', async (req, res) => {
let config = cache.get('config')
if (!config) {
config = await db.query('SELECT * FROM config WHERE id = 1')
cache.set('config', config)
}
res.json(config)
})
5. API Rate Limits
Check: Do you protect your endpoints from abuse?
// ✅ GOOD: Rate limiting
const rateLimit = require('express-rate-limit')
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: 'Too many requests from this IP'
})
app.use('/api/', limiter)
6. No Error Boundaries
Check: Does one failed API call bring down your entire app?
// ❌ BAD: Unhandled promise rejection crashes server
app.get('/external-data', async (req, res) => {
const data = await fetch('https://external-api.com/data')
res.json(await data.json())
})
// ✅ GOOD: Graceful error handling
app.get('/external-data', async (req, res) => {
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000) // 5s timeout
const response = await fetch('https://external-api.com/data', {
signal: controller.signal
})
clearTimeout(timeout)
if (!response.ok) {
return res.json({ data: fallbackData, source: 'fallback' })
}
res.json(await response.json())
} catch (error) {
console.error('External API failed:', error)
res.json({ data: cachedData || fallbackData, source: 'cached' })
}
})
7. Memory Leaks
Check: Does memory usage grow over time?
// Monitor memory usage
setInterval(() => {
const used = process.memoryUsage()
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
})
}, 60000) // Log every minute
// Common causes:
// ❌ Global arrays that grow indefinitely
// ❌ Event listeners not removed
// ❌ Unclosed database connections
// ❌ Large in-memory caches without eviction
Scaling Readiness Score
✅ Ready for production if you have:
- Connection pooling configured
- No N+1 queries in critical paths
- Database indexes on foreign keys and search fields
- Caching for frequently accessed data
- Rate limiting on public endpoints
- Error boundaries for external dependencies
- Load tested at 2-3x expected traffic
⚠️ Not ready if:
- Load test fails above 50 concurrent users
- Response times >2 seconds under load
- Database connections max out
- Memory usage grows indefinitely
- Any endpoint returns 500 errors under load
Phase 3: Migrate Your Database
If Using Replit Database:
Replit DB is key-value only. You'll need to:
- Export your data:
from replit import db
import json
data = {key: db[key] for key in db.keys()}
with open('export.json', 'w') as f:
json.dump(data, f)
- Choose a production database:
- Supabase - PostgreSQL, generous free tier, great for startups
- PlanetScale - MySQL, serverless, excellent scaling
- MongoDB Atlas - NoSQL, flexible schema
- Neon - Serverless Postgres, pay-per-use
- Redesign your schema (if moving from KV to SQL):
-- Example: Convert flat KV store to relational
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
title TEXT,
content TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
- Migrate your data:
// migration script
const data = require('./export.json')
for (const [key, value] of Object.entries(data)) {
if (key.startsWith('user_')) {
await db.insert({ ...value, table: 'users' })
}
}
If Using PostgreSQL on Replit:
- Dump your database:
pg_dump $DATABASE_URL > backup.sql
- Import to new database:
psql $NEW_DATABASE_URL < backup.sql
Phase 4: Deploy to Production
Deploying to Vercel:
- Install Vercel CLI:
npm install -g vercel
- Deploy:
vercel
# Follow prompts to link to GitHub repo
- Set environment variables:
vercel env add DATABASE_URL
vercel env add API_KEY
- Configure domains:
- Go to Vercel dashboard
- Add your custom domain
- Update DNS records
Deploying to Railway:
-
Connect GitHub: Go to railway.app, connect your repo
-
Add database: Click "New" → "Database" → "PostgreSQL"
-
Set variables: Click your service → Variables → Add all your env vars
-
Deploy: Railway auto-deploys on push to main
Deploying to AWS:
- Use AWS Amplify (easiest):
npm install -g @aws-amplify/cli
amplify init
amplify add hosting
amplify publish
- Or use Elastic Beanstalk:
- Package your app
- Upload to EB console
- Configure environment
- Deploy
Phase 5: Set Up Production Essentials
1. Monitoring
Sentry for error tracking:
npm install @sentry/nextjs
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV
})
Uptime monitoring:
- BetterUptime (free tier)
- UptimeRobot (free tier)
- Pingdom (paid)
2. Logging
LogRocket or Datadog:
LogRocket.init('your-app-id')
3. Analytics
// Google Analytics
import { Analytics } from '@vercel/analytics/react'
export default function App() {
return (
<>
<YourApp />
<Analytics />
</>
)
}
4. Backups
Database backups:
- Supabase: Automatic daily backups
- PlanetScale: Automatic with retention
- Custom: Set up cron jobs
# Daily backup script
0 2 * * * pg_dump $DATABASE_URL | gzip > backup-$(date +\%Y\%m\%d).sql.gz
5. CI/CD Pipeline
GitHub Actions (.github/workflows/deploy.yml):
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm install
- run: npm test
- run: npm run build
- name: Deploy to Vercel
run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
Phase 6: Security Hardening
1. Enable HTTPS
- Vercel/Railway: Automatic
- Custom server: Use Let's Encrypt + Certbot
2. Set Security Headers
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' }
]
}
]
}
}
3. Rate Limiting
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per window
})
app.use('/api/', limiter)
4. Secrets Management
- Use environment variables (never commit secrets)
- Consider AWS Secrets Manager or HashiCorp Vault for enterprise
Phase 7: Test Everything
Load Testing
# Install k6
brew install k6
# Create test script
cat > load-test.js << 'EOF'
import http from 'k6/http';
import { check } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 0 }, // Ramp down
],
};
export default function() {
let res = http.get('https://yourapp.com');
check(res, { 'status was 200': (r) => r.status == 200 });
}
EOF
# Run test
k6 run load-test.js
Security Scanning
# npm audit
npm audit --production
# Snyk
npx snyk test
# OWASP Dependency Check
dependency-check --project "My App" --scan .
Phase 8: Go Live
Launch Checklist
- DNS configured and propagated
- SSL certificate active
- Environment variables set
- Database migrated and tested
- Monitoring tools configured
- Backups scheduled
- Load testing passed
- Security scan completed
- Error handling tested
- Team trained on new infrastructure
Migration Strategy
Option A: Big Bang (Small apps)
- Schedule maintenance window
- Migrate everything at once
- Switch DNS
- Monitor closely
Option B: Gradual (Larger apps)
- Deploy new infrastructure
- Run parallel for testing
- Migrate users in batches
- Monitor both systems
- Final cutover when confident
Common Pitfalls
1. Forgetting Environment Variables
Set them before deploying, not after users start complaining.
2. Database Connection Pooling
Replit limits don't apply to production. Set max connections appropriately.
3. Static Assets
Host images/files on CDN (Cloudflare, AWS S3), not your server.
4. CORS Configuration
Update allowed origins for your new domain.
5. Webhook URLs
Update all third-party services (Stripe, SendGrid, etc.) with new endpoints.
Post-Migration Optimization
Week 1:
- Monitor error rates and performance
- Fix critical issues
- Optimize slow queries
Month 1:
- Analyze costs and optimize
- Set up additional monitoring
- Document architecture
Month 3:
- Review and refactor based on usage patterns
- Plan scaling strategy
- Update team documentation
Cost Comparison
Replit (example app):
- Hacker Plan: $7/month
- Deployments: $10-40/month
- Database: Included Total: ~$20-50/month
Vercel + Supabase:
- Vercel Pro: $20/month
- Supabase Free/Pro: $0-25/month Total: $20-45/month (more features, better performance)
Railway (all-in-one):
- $5 base + usage
- Database included Total: $15-40/month typically
Conclusion
Migrating from Replit to production infrastructure is a rite of passage. It means your app has graduated from prototype to product.
The key steps:
- Choose the right platform for your needs
- Prepare your code and data
- Set up proper infrastructure
- Test thoroughly before cutover
- Monitor closely after launch
Done right, migration unlocks better performance, security, and scalability. Your users will thank you.
Need expert help migrating your Replit app? We've done this dozens of times. Get a free migration plan.
About the Author
DomAIn Labs Team
The DomAIn Labs team consists of AI engineers, strategists, and educators passionate about demystifying AI for small businesses.