| name | production-harden |
| description | Transform vibe-coded MVPs into production-grade, reusable templates. Systematically audit and fix the Five Mortal Sins of production code. |
| triggers | ["/production-harden","/harden","make this production ready","turn this into a template"] |
Production Hardening Skill
Purpose: Systematically transform a working MVP into a production-grade, reusable template by addressing the Five Mortal Sins of vibe-coded projects.
When to Use
- After completing an MVP that "works but feels fragile"
- Before turning a project into a reusable template
- When preparing for real users or production deployment
- After a Mars audit reveals issues
The Five Mortal Sins (Audit Checklist)
1. Data Model Drift
2. Happy Path Delusion
3. Observability Void
4. Environment Confusion
5. Type Safety Gaps
Execution Process
Phase 1: Audit (Read-Only)
1. Run /mars or manually check each sin
2. Document all findings by severity:
- CRITICAL: Security/data loss risks
- SEVERE: Will cause outages
- MODERATE: Degraded experience
- MINOR: Technical debt
3. Create prioritized todo list
Phase 2: Core Fixes
Error Boundaries (Next.js App Router)
Create these files in src/app/:
error.tsx - Catches route errors
"use client";
import { useEffect } from "react";
interface ErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function Error({ error, reset }: ErrorProps) {
useEffect(() => {
console.error("Application error:", error);
}, [error]);
return (
<div className="min-h-[50vh] flex flex-col items-center justify-center p-8">
<h1 className="text-2xl font-bold mb-4">Something went wrong</h1>
<button onClick={reset} className="px-4 py-2 bg-blue-500 text-white rounded">
Try again
</button>
</div>
);
}
global-error.tsx - Catches root layout errors
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h1>Critical Error</h1>
<button onClick={reset}>Try again</button>
</body>
</html>
);
}
not-found.tsx - Custom 404
import Link from "next/link";
export default function NotFound() {
return (
<div className="min-h-[50vh] flex flex-col items-center justify-center">
<h1 className="text-4xl font-bold mb-4">404</h1>
<p className="mb-4">Page not found</p>
<Link href="/" className="text-blue-500 hover:underline">
Go home
</Link>
</div>
);
}
Image Fallback Component
"use client";
import Image from "next/image";
import { useState } from "react";
const FALLBACK_IMAGE = "/images/placeholder.png";
interface GameImageProps {
src: string;
alt: string;
fill?: boolean;
width?: number;
height?: number;
sizes?: string;
className?: string;
}
export function GameImage({ src, alt, fill, width, height, sizes, className = "object-contain" }: GameImageProps) {
const [imgSrc, setImgSrc] = useState(src);
const [hasError, setHasError] = useState(false);
const handleError = () => {
if (!hasError) {
setHasError(true);
setImgSrc(FALLBACK_IMAGE);
}
};
if (fill) {
return <Image src={imgSrc} alt={alt} fill sizes={sizes} className={className} onError={handleError} />;
}
return <Image src={imgSrc} alt={alt} width={width} height={height} className={className} onError={handleError} />;
}
Zod Validation for JSON Data
import { z } from "zod";
export const ItemSchema = z.object({
name: z.string(),
rarity: z.enum(["Common", "Rare", "Epic", "Legendary"]),
description: z.string().nullable(),
});
export const ItemsFileSchema = z.record(z.string(), ItemSchema);
export function validateData<T>(schema: z.ZodType<T>, data: unknown, name: string): T {
const result = schema.safeParse(data);
if (!result.success) {
console.error(`Validation failed for ${name}:`, result.error.format());
throw new Error(`Invalid ${name} data`);
}
return result.data;
}
import { validateData, ItemsFileSchema } from "./schemas";
import itemsData from "../../data/items.json";
const validatedItems = validateData(ItemsFileSchema, itemsData, "items");
Generic Filter Components
interface FilterBarProps<T extends string> {
options: { value: T; label: string }[];
selected: T[];
onChange: (selected: T[]) => void;
}
export function FilterBar<T extends string>({ options, selected, onChange }: FilterBarProps<T>) {
}
const [selectedTags, setSelectedTags] = useState<ItemTag[]>([]);
<FilterBar<ItemTag> options={tagOptions} selected={selectedTags} onChange={setSelectedTags} />
SEO Metadata Pattern
import type { Metadata } from "next";
import { ItemsContent } from "@/components/pages/items-content";
export const metadata: Metadata = {
title: "Items | My App",
description: "Browse all items with filtering and search.",
openGraph: {
title: "Items | My App",
description: "Browse all items with filtering and search.",
},
};
export default function ItemsPage() {
return <ItemsContent />;
}
Accessibility Additions
<article aria-label={`${item.name} - ${item.rarity}`}>
{}
</article>
<button
aria-pressed={isSelected}
aria-label={`Filter by ${option.label}`}
>
{option.label}
</button>
<div role="group" aria-label="Filter by rarity">
{/* filter buttons */}
</div>
Phase 3: Environment & Config
.env.example
Verify tsconfig.json
{
"compilerOptions": {
"strict": true,
}
}
Phase 4: Verify & Deploy
npx tsc --noEmit
npm run build
vercel --prod
Key Patterns Learned
Pattern: Zod Catches Data Drift
When Zod validation fails during build, it reveals real issues:
- Missing enum values (add to both types.ts and schemas.ts)
- Nullable vs optional fields (use
.nullable() not .optional())
- Missing required fields
Pattern: Client/Server Component Split
For pages needing both metadata and interactivity:
- Keep page.tsx as server component (exports metadata)
- Move interactive logic to separate client component
- Import and render client component from page.tsx
Pattern: Centralized Constants
export const RARITY_ORDER = ["Common", "Rare", "Epic", "Legendary"] as const;
export const rarityColors: Record<Rarity, string> = {
Common: "text-gray-500",
Rare: "text-blue-500",
};
Typical Timeline
| Phase | Tasks | Time |
|---|
| Audit | Review all files, document issues | 15-30 min |
| Error Handling | Boundaries, fallbacks, validation | 30-60 min |
| Type Safety | Fix assertions, add Zod | 30-60 min |
| Polish | SEO, accessibility, .env | 20-30 min |
| Deploy | Build, test, deploy | 10-15 min |
Total: 2-3 hours for a typical MVP
Success Criteria