Skip to main content
INS // Insights

Productionize AI Prototype: Gaps That Break Vibe Code

Updated June 2026 · 5 min read

AI-assisted development tools — Bolt, Lovable, Cursor, v0 — have genuinely changed what's possible for a non-engineer to prototype. In a weekend, you can have a functional-looking application that demos well, has reasonable UI, and handles the happy path. This is legitimately impressive.

It is also not production software. The gaps between a prototype and something you can give to real users — at scale, reliably, securely — are consistently the same five things. This post names them and explains what closing them looks like.

Gap 1: No Authentication That's Actually Secure

AI-generated prototypes frequently implement authentication in ways that work in demos but create real liability in production.

Common problems: - JWTs not properly validated (accepting expired tokens, not verifying signatures) - Sessions stored in localStorage (XSS-vulnerable) - Password hashing with MD5 or SHA1 instead of bcrypt/argon2 - No rate limiting on authentication endpoints (brute-force trivially possible) - No email verification flow - Password reset via predictable tokens

What production authentication requires: - Use an established auth provider (Cognito, Auth0, Supabase Auth, Clerk) rather than implementing from scratch. The edge cases in authentication security are extensive and unforgiving. - If custom JWT validation: verify signature + expiration + audience claim. Never trust client-sent data without verification. - Argon2 or bcrypt for password hashing. Not SHA256. - Rate limiting on /login, /register, /password-reset endpoints. - HTTPS only. Redirect HTTP to HTTPS.

// Bad: JWT from prototype - no signature verification
const decoded = JSON.parse(atob(token.split('.')[1]));
const userId = decoded.sub; // Trusts whatever's in the token

// Good: Proper JWT verification
import { verify } from 'jsonwebtoken';

function getUserFromToken(token: string): User {
  const payload = verify(token, process.env.JWT_SECRET!, {
    algorithms: ['HS256'],
    audience: 'myapp.com',
    issuer: 'auth.myapp.com'
  });
  return payload as User;
}

Gap 2: No Error Handling at Boundaries

Prototype code assumes inputs are well-formed. Production code receives malformed inputs constantly — from bugs, from users, from integration partners, from network failures.

What breaks without error handling: - Unhandled promise rejections that crash Node.js workers - Database errors surfaced directly to users (with stack traces that reveal schema) - Downstream API failures that cascade into total application failure - Validation errors that produce undefined is not an object instead of a useful message

What production error handling looks like:

// Prototype: assumes everything works
async function processOrder(orderId: string) {
  const order = await db.orders.findById(orderId);
  const payment = await stripe.charges.create({...});
  await db.orders.update(orderId, { status: 'paid' });
  await sendConfirmationEmail(order.email);
}

// Production: handles each failure mode
async function processOrder(orderId: string): Promise<Result<Order, Error>> {
  const order = await db.orders.findById(orderId);
  if (!order) {
    return { ok: false, error: new NotFoundError(`Order ${orderId} not found`) };
  }

  let payment;
  try {
    payment = await stripe.charges.create({...});
  } catch (e) {
    if (e instanceof Stripe.errors.StripeCardError) {
      return { ok: false, error: new PaymentError(e.message) };
    }
    // Unexpected Stripe error - log with context, don't expose details
    logger.error({ event: 'stripe_error', orderId, error: e });
    return { ok: false, error: new InternalError('Payment processing unavailable') };
  }

  await db.orders.update(orderId, { status: 'paid', stripeChargeId: payment.id });

  // Email failure should not roll back payment
  await sendConfirmationEmail(order.email).catch(e => {
    logger.warn({ event: 'email_failed', orderId, error: e });
  });

  return { ok: true, value: order };
}

Gap 3: No Production Infrastructure

AI prototype apps run on localhost:3000. Production requires:

Deployment target: Vercel/Railway/Render for simple apps are acceptable. For anything needing custom infrastructure, ECS Fargate or Lambda behind API Gateway.

Environment configuration: Prototype has API keys in .env files. Production uses Secrets Manager with rotation, environment-specific configs, and no secrets in source code.

Database that's production-grade: SQLite for prototypes → RDS Aurora, Neon, or PlanetScale for production. This means migration management (not schema resets), connection pooling, and backup configuration.

CDN for static assets: CloudFront or similar. Direct S3 serving is too slow and too expensive at scale.

Health checks and monitoring: Load balancers need /health endpoints. CloudWatch needs metrics for error rates, latency, and key business metrics. Pager alerts for failures at 2 AM.

Gap 4: No Input Validation

SQL injection. XSS. Path traversal. Server-side request forgery. These attacks work on code that trusts user input. Prototype code trusts user input.

// Bad: Prototype trusts everything
const searchQuery = req.query.q;
const results = await db.query(`SELECT * FROM products WHERE name LIKE '%${searchQuery}%'`);

// Good: Parameterized queries + validation
import { z } from 'zod';

const SearchSchema = z.object({
  q: z.string().min(1).max(100).trim()
});

const { q } = SearchSchema.parse(req.query);
const results = await db.query(
  'SELECT * FROM products WHERE name ILIKE $1',
  [`%${q}%`]
);

Every external input — query parameters, request bodies, headers, file uploads — is untrusted until validated. Zod, Joi, Yup, or Pydantic (Python) are the validation libraries of choice.

For file uploads: validate MIME type (not the file extension, which is user-controlled), scan for malware, limit size, store in S3 not on the server filesystem.

Gap 5: No Observability

When something breaks in production, you need to know three things: 1. What happened (error) 2. Where it happened (stack trace + context) 3. How often it happened (frequency)

Prototypes have console.log. Production needs:

Structured logging: JSON-formatted log entries with timestamp, request ID, user ID, and relevant context. CloudWatch Logs + Log Insights for querying.

Error tracking: Sentry or equivalent — captures uncaught errors with full stack trace, request context, and user impact. Essential for diagnosing production issues.

Request tracing: Correlation ID passed through all log entries for a single request. When investigating "user X reported an error at 3:47 PM," you can pull every log entry for that request.

Metrics and dashboards: CloudWatch custom metrics for business-level signals — new signups, order volume, prediction count — not just infrastructure metrics.

Alerting: Threshold-based alerts for error rate spikes, latency p99 increases, and business metric drops.

The Production Hardening Timeline

For a typical AI-generated prototype, the path to production:

  • Week 1–2: Authentication audit and hardening; input validation layer; database migration to production-grade DB
  • Week 2–3: Infrastructure setup — deployment pipeline, environment configuration, secrets management
  • Week 3–4: Error handling pass through critical paths; observability setup (logging, error tracking, monitoring)
  • Week 4–5: Load testing, security review, performance optimization
  • Week 5–6: Soft launch with limited users; monitoring validation; fix issues surfaced under real traffic

See our full-stack development capability and React TypeScript production patterns for the engineering standards we apply.

Frequently Asked Questions

Can AI-generated code actually be used in production?

The code itself is often fine — LLMs write decent syntax. The problems are architectural (missing validation, error handling) and operational (no deployment infrastructure, no observability). Both are fixable. The question is whether fixing them is faster than a targeted rebuild of the critical paths.

Is it worth saving an AI prototype or better to start clean?

If the prototype has achieved product-market signal (users like it, it captures a real workflow), the business logic embedded in it has value. Build around what works. The parts most worth saving: data model (expensive to change), core UI flows, working integrations. The parts most worth rebuilding: auth, infrastructure, validation.

How do we prevent this problem on the next prototype?

Use prototyping tools for prototypes. When you reach the point where real users or sensitive data are involved, that's the transition point. Don't try to boil the ocean on the prototype — build it fast, validate the idea, then do the engineering work properly.

What's the most dangerous production gap?

Authentication and input validation tie for first place. An app with a broken auth system exposes all user data. An app with SQL injection exposes the entire database. Both are common in prototype code and both have been exploited in the wild on apps that "worked fine" in testing.

Do you work with any AI-generated codebase, or only specific tools?

We work with any codebase. AI-generated code from Bolt, Cursor, GitHub Copilot, or Claude looks similar under the hood — it's usually readable, somewhat structured, and missing the production concerns above. The assessment approach is the same regardless of generation source.


Book a project diagnostic → rutagon.com/contact

Ready to discuss your project?

We deliver production-grade software for government, defense, and commercial clients. Let's talk about what you need.

Initiate Contact