tools

Bolt.new Apps: 7 Production Issues and How to Fix Them

Bolt.new gets you from idea to working app in minutes. But the generated code has consistent patterns that break in production. Here are 7 specific issues with code-level fixes.

FinishKit Team10 min read

Bolt.new hit 5 million users in five months. The appeal is obvious: open a browser, describe what you want, and watch a working app materialize in real-time. No setup, no terminal, no configuration. Just ideas becoming code becoming a visible product.

It's genuinely one of the best tools available for rapid prototyping and idea validation. If you need to see something working in the next five minutes, Bolt is hard to beat.

But there's a consistent pattern with Bolt-generated code: it optimizes for working quickly rather than working well. The same shortcuts that make Bolt fast at generating apps create specific, predictable issues when you try to run those apps in production.

We've analyzed dozens of Bolt-generated projects and found the same seven issues appearing over and over. Here's what they are and exactly how to fix each one.

1. Inline Styles Instead of Design Tokens

The problem: Bolt generates components with hardcoded inline styles. Colors, spacing, font sizes, and breakpoints are scattered as literal values across hundreds of components.

// Typical Bolt output
const Header = () => (
  <header style={{ backgroundColor: '#6366f1', padding: '16px 24px' }}>
    <h1 style={{ color: '#ffffff', fontSize: '24px', fontWeight: 'bold' }}>
      My App
    </h1>
    <nav style={{ display: 'flex', gap: '16px' }}>
      <a style={{ color: '#e0e7ff', fontSize: '14px' }}>Dashboard</a>
      <a style={{ color: '#e0e7ff', fontSize: '14px' }}>Settings</a>
    </nav>
  </header>
);

Why it breaks in production: When you need to change your primary color, adjust spacing for mobile, or implement dark mode, you're editing dozens of files. Inconsistencies creep in. Your app looks slightly different on every page.

The fix: Extract values into a design system. If using Tailwind (which Bolt sometimes uses but inconsistently):

// tailwind.config.ts - define your tokens once
const config = {
  theme: {
    extend: {
      colors: {
        primary: { DEFAULT: '#6366f1', light: '#e0e7ff' },
      },
    },
  },
};
 
// Use tokens everywhere
const Header = () => (
  <header className="bg-primary px-6 py-4">
    <h1 className="text-white text-2xl font-bold">My App</h1>
    <nav className="flex gap-4">
      <a className="text-primary-light text-sm">Dashboard</a>
      <a className="text-primary-light text-sm">Settings</a>
    </nav>
  </header>
);

2. No Error Boundaries

The problem: Bolt generates React components without error boundaries. When any component throws a rendering error, the entire application crashes to a white screen.

// Bolt generates components that assume data is always present
const UserProfile = ({ user }) => (
  <div>
    <h2>{user.name}</h2>
    <p>{user.email}</p>
    <p>Member since {new Date(user.created_at).toLocaleDateString()}</p>
    {/* If user is null or created_at is invalid, the entire app crashes */}
  </div>
);

Why it breaks in production: In production, data is sometimes null. API calls sometimes fail. User records are sometimes incomplete. A single null value in a single component takes down the entire application for that user.

The fix: Add error boundaries and defensive rendering:

// ErrorBoundary.tsx
import { Component, type ReactNode } from 'react';
 
export class ErrorBoundary extends Component<
  { children: ReactNode; fallback?: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };
 
  static getDerivedStateFromError() {
    return { hasError: true };
  }
 
  componentDidCatch(error: Error) {
    // Report to error tracking service
    reportError(error);
  }
 
  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div className="p-4 text-center">
          <p>Something went wrong. Please refresh the page.</p>
        </div>
      );
    }
    return this.props.children;
  }
}
 
// Wrap sections of your app
<ErrorBoundary fallback={<ProfileError />}>
  <UserProfile user={user} />
</ErrorBoundary>

Also add defensive checks in components:

const UserProfile = ({ user }) => {
  if (!user) return <EmptyState message="User not found" />;
 
  return (
    <div>
      <h2>{user.name ?? 'Unknown'}</h2>
      <p>{user.email ?? 'No email'}</p>
      {user.created_at && (
        <p>Member since {new Date(user.created_at).toLocaleDateString()}</p>
      )}
    </div>
  );
};

3. Missing Loading States

The problem: Bolt generates components that fetch data and render it, but skip the intermediate states: loading and error. The component either shows data or shows nothing.

// Typical Bolt output
const Dashboard = () => {
  const [projects, setProjects] = useState([]);
 
  useEffect(() => {
    fetch('/api/projects')
      .then(res => res.json())
      .then(data => setProjects(data));
  }, []);
 
  return (
    <div>
      {projects.map(p => <ProjectCard key={p.id} project={p} />)}
    </div>
  );
};

Why it breaks in production: On slow connections, users see a blank page for seconds while data loads. They don't know if the app is broken or just slow. They click buttons repeatedly. They leave.

The fix:

const Dashboard = () => {
  const [projects, setProjects] = useState<Project[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
 
  useEffect(() => {
    fetch('/api/projects')
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => setProjects(data))
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, []);
 
  if (loading) return <DashboardSkeleton />;
  if (error) return <ErrorState message="Failed to load projects" />;
  if (projects.length === 0) return <EmptyState message="No projects yet" />;
 
  return (
    <div>
      {projects.map(p => <ProjectCard key={p.id} project={p} />)}
    </div>
  );
};

Skeleton loaders (animated placeholder shapes that match your content layout) are much better than spinners. They give users a sense of what's coming and feel significantly faster. Libraries like react-loading-skeleton make this easy.

4. No Input Validation

The problem: Bolt generates forms that send user input directly to the backend without any validation. No type checking, no length limits, no format validation.

// Typical Bolt form
const CreateProject = () => {
  const [name, setName] = useState('');
 
  const handleSubmit = async () => {
    await fetch('/api/projects', {
      method: 'POST',
      body: JSON.stringify({ name }),
    });
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button type="submit">Create</button>
    </form>
  );
};

Why it breaks in production: Users submit empty forms. Users paste enormous strings. Automated bots submit garbage data. Your database fills with invalid records. Your app crashes trying to render unexpected data.

The fix: Validate on both client and server:

import { z } from 'zod';
 
const ProjectSchema = z.object({
  name: z.string()
    .min(1, 'Project name is required')
    .max(100, 'Name must be under 100 characters')
    .trim(),
});
 
const CreateProject = () => {
  const [name, setName] = useState('');
  const [error, setError] = useState('');
  const [submitting, setSubmitting] = useState(false);
 
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
 
    const result = ProjectSchema.safeParse({ name });
    if (!result.success) {
      setError(result.error.issues[0].message);
      return;
    }
 
    setSubmitting(true);
    try {
      const res = await fetch('/api/projects', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(result.data),
      });
      if (!res.ok) throw new Error('Failed to create project');
    } catch {
      setError('Failed to create project. Please try again.');
    } finally {
      setSubmitting(false);
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        maxLength={100}
        required
      />
      {error && <p className="text-red-500 text-sm">{error}</p>}
      <button type="submit" disabled={submitting}>
        {submitting ? 'Creating...' : 'Create'}
      </button>
    </form>
  );
};

5. Hardcoded Values

The problem: Bolt hardcodes values that should be configurable: API URLs, feature flags, pricing tiers, limits, and configuration values are scattered as string literals throughout the codebase.

// Scattered across the codebase
const API_RESPONSE = await fetch('https://api.example.com/v1/data');
const MAX_ITEMS = 50;
const PRICE_PER_MONTH = 29;
const SUPPORT_EMAIL = 'help@example.com';

Why it breaks in production: When you need to change the API URL for staging vs production, update your pricing, or change a limit, you're searching and replacing across dozens of files. You'll miss one. It will cause a bug.

The fix: Centralize configuration:

// lib/config.ts
export const config = {
  api: {
    baseUrl: process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000/api',
  },
  limits: {
    maxItemsPerPage: 50,
    maxFileUploadMB: 10,
  },
  billing: {
    proMonthlyPrice: 29,
    teamMonthlyPrice: 79,
  },
  support: {
    email: 'help@yourapp.com',
  },
} as const;
 
// Use it everywhere
import { config } from '@/lib/config';
const res = await fetch(`${config.api.baseUrl}/data`);

6. No Tests

The problem: Bolt generates zero tests. No unit tests, no integration tests, no end-to-end tests. The generated project often doesn't even include a test runner in its dependencies.

Why it breaks in production: Without tests, every deployment is a prayer. You have no automated way to verify that core functionality works after making changes. As you modify and extend the code, regressions pile up silently.

The fix: You don't need 100% coverage. Start with tests for the most critical paths:

// __tests__/api/projects.test.ts
import { describe, it, expect } from 'vitest';
 
describe('Projects API', () => {
  it('requires authentication', async () => {
    const res = await fetch('/api/projects');
    expect(res.status).toBe(401);
  });
 
  it('returns only the authenticated user projects', async () => {
    const res = await fetch('/api/projects', {
      headers: { Authorization: `Bearer ${testUserToken}` },
    });
    const data = await res.json();
    expect(data.every((p: any) => p.userId === testUserId)).toBe(true);
  });
 
  it('validates input on creation', async () => {
    const res = await fetch('/api/projects', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${testUserToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name: '' }),
    });
    expect(res.status).toBe(400);
  });
});

Install a test runner if one isn't present:

npm install -D vitest @testing-library/react @testing-library/jest-dom

7. No Deployment Configuration

The problem: Bolt generates the app code but provides no deployment configuration. No Dockerfile, no vercel.json, no CI/CD pipeline, no environment variable documentation.

Why it breaks in production: Deployment becomes a manual, error-prone process. Environment variables are missing or wrong. Build optimizations aren't applied. There's no automated pipeline to catch issues before they reach production.

The fix: Add deployment configuration appropriate for your hosting provider. For Vercel:

// vercel.json
{
  "framework": "nextjs",
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Strict-Transport-Security", "value": "max-age=63072000" }
      ]
    }
  ]
}

Create a .env.example documenting every required variable:

# .env.example - copy to .env.local and fill in values
DATABASE_URL=
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

If you're deploying a Bolt app to production, you've likely already moved the code out of Bolt's browser environment and into a proper repo. At that point, treat it like any other codebase that needs hardening. The fact that it was generated by Bolt doesn't make the production requirements any different.

The Pattern

All seven of these issues stem from the same root cause: Bolt optimizes for speed and visual output. That's what makes it magical for prototyping. But speed and visual output are not what production software needs. Production software needs reliability, security, maintainability, and graceful failure handling.

These aren't criticisms of Bolt. They're the natural trade-offs of a tool designed to turn ideas into visible apps as fast as possible. The comparison between Cursor, Lovable, and Bolt covers this pattern in more depth: every AI build tool makes similar trade-offs, just in different areas.

The fix for each issue is straightforward. The challenge is knowing which issues exist in your specific codebase and prioritizing them correctly.

FinishKit scans your Bolt-generated repo and identifies exactly these kinds of issues, plus security gaps, missing auth checks, and deploy configuration problems. You get a prioritized Finish Plan that tells you what to fix first. Scan your repo and see what needs attention before launch.

Bolt built it fast. Now make it last.