Towering mountain peaks piercing through clouds
Vibe Coding

Migrating Your Replit App to Production: A Complete Guide

DomAIn Labs Team
January 7, 2025
10 min read

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:

  1. 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)
  1. 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
  1. 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()
);
  1. 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:

  1. Dump your database:
pg_dump $DATABASE_URL > backup.sql
  1. Import to new database:
psql $NEW_DATABASE_URL < backup.sql

Phase 4: Deploy to Production

Deploying to Vercel:

  1. Install Vercel CLI:
npm install -g vercel
  1. Deploy:
vercel
# Follow prompts to link to GitHub repo
  1. Set environment variables:
vercel env add DATABASE_URL
vercel env add API_KEY
  1. Configure domains:
  • Go to Vercel dashboard
  • Add your custom domain
  • Update DNS records

Deploying to Railway:

  1. Connect GitHub: Go to railway.app, connect your repo

  2. Add database: Click "New" → "Database" → "PostgreSQL"

  3. Set variables: Click your service → Variables → Add all your env vars

  4. Deploy: Railway auto-deploys on push to main

Deploying to AWS:

  1. Use AWS Amplify (easiest):
npm install -g @aws-amplify/cli
amplify init
amplify add hosting
amplify publish
  1. 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:

  1. Choose the right platform for your needs
  2. Prepare your code and data
  3. Set up proper infrastructure
  4. Test thoroughly before cutover
  5. 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.

Tags:Replitmigrationproductiondeploymentinfrastructure

About the Author

DomAIn Labs Team

The DomAIn Labs team consists of AI engineers, strategists, and educators passionate about demystifying AI for small businesses.