Project Overview
One sentence: what is this project, what tech stack, what stage is it in?
[Product Name] is a [type] built with [framework], [database], [auth], [payments]. Currently in [stage: early development / beta / production].
Commands
List your actual development commands. Delete any that don't apply.
# Development — run these in separate terminals
[your dev server command] # e.g., npm run dev
[your backend command] # e.g., npx convex dev
# Database
[your db command] # e.g., npx convex dashboard
[your migration command] # e.g., npx convex deploy
# Build & Test
[your build command] # e.g., npm run build
[your lint command] # e.g., npm run lint
[your test command] # e.g., npm run test
# Deployment
[your deploy command] # e.g., vercel --prod
[your backend deploy command] # e.g., npx convex deployArchitecture Rules
Backend Patterns
Document the rules for your backend framework. Include DO and DON'T code examples.
Queries (data reads):
- [How queries work in your framework]
- [What they can and can't do]
// DO: Pure data reads with auth validation
export const getByUser = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthenticated");
// ... fetch and return data
},
});
// DON'T: Side effects in queries
export const getByUser = query({
handler: async (ctx) => {
await sendEmail(); // WRONG — queries must be pure
},
});Mutations (data writes):
- [How mutations work]
- [What they can and can't do]
// DO: Validate auth + ownership, then write
export const update = mutation({
args: { id: v.id("projects"), name: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthenticated");
const project = await ctx.db.get(args.id);
if (project?.userId !== user._id) throw new Error("Unauthorized");
await ctx.db.patch(args.id, { name: args.name });
},
});
// DON'T: Call external APIs in mutations
export const update = mutation({
handler: async (ctx, args) => {
await fetch("https://api.example.com"); // WRONG — use actions for external calls
},
});Actions (external API calls):
- [How actions work]
- [When to use actions vs. mutations]
// DO: Call external API, then store result via internal mutation
export const chat = action({
args: { projectId: v.id("projects"), messages: v.array(v.any()) },
handler: async (ctx, args) => {
const response = await fetch("https://api.openrouter.ai/...");
const data = await response.json();
await ctx.runMutation(internal.messages.store, {
projectId: args.projectId,
content: data.choices[0].message.content,
});
},
});
// DON'T: Write to DB directly in an action
export const chat = action({
handler: async (ctx, args) => {
await ctx.db.insert("messages", {}); // WRONG — ctx.db doesn't exist in actions
},
});Frontend Patterns
- Routing: [Your routing approach, e.g., file-based App Router]
- Server vs. Client Components: [When to use each]
- Data fetching: [Hooks, server components, API calls]
// DO: Handle all three states for reactive data
const data = useQuery(api.projects.getByUser);
if (data === undefined) return <Skeleton />; // Loading
if (data === null) return <EmptyState />; // Not found
return <ProjectList projects={data} />; // Data ready
// DON'T: Ignore loading/error states
const data = useQuery(api.projects.getByUser);
return <ProjectList projects={data} />; // WRONG — crashes when data is undefinedComponent Conventions
- File naming: kebab-case for files (
chat-panel.tsx), PascalCase for components (ChatPanel) - Props: Interfaces for component props, types for unions/utilities
- Imports order: React → third-party → local components → local utils → types
// Standard component structure for this project
"use client";
import { useState } from "react";
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { Id } from "@/convex/_generated/dataModel";
interface ChatPanelProps {
projectId: Id<"projects">;
stageKey: string;
}
export function ChatPanel({ projectId, stageKey }: ChatPanelProps) {
const messages = useQuery(api.messages.getByStage, { projectId, stageKey });
const [input, setInput] = useState("");
if (messages === undefined) return <ChatSkeleton />;
return (
<div className="flex flex-col gap-4">
{/* ... */}
</div>
);
}Styling
- System: Tailwind CSS only — no CSS modules, styled-components, or inline styles
- Component library: shadcn/ui — use it for all standard UI (buttons, inputs, dialogs, dropdowns)
- Theme: Dark-first design
- Utility: Use
cn()from@/lib/utilsfor conditional classes - Icons: lucide-react
Design Tokens
/* List your actual CSS variables */
--background: [value];
--foreground: [value];
--primary: [value];
--accent: [value];
--muted: [value];
--destructive: [value];TypeScript
- Strict mode: Yes —
tsconfig.jsonhasstrict: true - No
anytypes. Useunknownand narrow, or define proper types. - No
@ts-ignore. Fix the type error instead. - Use generated types: Import
Id,Docfrom@/convex/_generated/dataModel
Known Gotchas
Document bugs you've encountered, weird behaviors, and how you fixed them. Claude reads this and avoids repeating them.
[Gotcha 1: Title]
- Symptom: (What went wrong)
- Cause: (Why it happened)
- Fix: (How you solved it)
- Rule: (What to do going forward to prevent it)
[Gotcha 2: Title]
- Symptom:
- Cause:
- Fix:
- Rule:
[Gotcha 3: Title]
- Symptom:
- Cause:
- Fix:
- Rule:
Add new gotchas as you encounter them. This section prevents Claude from making the same mistakes twice.
Feature Documentation
For each major feature, document how it works, which files are involved, and any gotchas.
Feature: [Name]
- What it does: (One sentence)
- Files involved:
- convex/[file].ts — backend functions
- components/[file].tsx — UI component
- app/[route]/page.tsx — page
- Data flow: User does X → calls Y mutation → stores Z → triggers re-render via query
- Gotchas: (Anything Claude should know when modifying this feature)
Feature: [Name]
- What it does:
- Files involved:
-
- Data flow:
- Gotchas:
Feature: [Name]
- What it does:
- Files involved:
-
- Data flow:
- Gotchas:
Error Handling
- API errors: Wrap all external API calls in try/catch. On failure, store error message and show user-friendly toast.
- Form validation: Validate on submit with clear inline error messages.
- Loading states: Show skeletons for data queries, spinners for actions. Never show a blank screen.
- Auth errors: Redirect to sign-in page. Never show raw auth errors.
Security Rules
Specific rules with numbers. Not checkboxes — enforced limits.
- Auth validation on every query, mutation, and action that touches user data
- Ownership check after loading any resource:
resource.userId === currentUser._id - Rate limit AI actions: max [X] requests per minute per user
- Rate limit auth: max [X] attempts per minute per IP
- Max upload size: [X] MB
- Input string length limits: [X] characters for names, [X] for descriptions, [X] for messages
- API keys in environment variables only, never in client-side code
- No raw user input in AI system prompts — always use a template with sanitized variables
- Webhook signatures verified before processing (Stripe via
stripe.webhooks.constructEvent, Clerk via svix)
Environment Variables
# List every required env var, where to get it, and which environment needs it
# Auth — [Provider] dashboard → API Keys
[VAR_NAME]= # Needed in: [Next.js / Convex / Both]
# Database — [Provider] dashboard → Settings
[VAR_NAME]= # Needed in: [Next.js / Convex / Both]
# Payments — Stripe dashboard → Developers → API Keys
[VAR_NAME]= # Needed in: [Next.js / Convex / Both]
# AI — [Provider] dashboard → API Keys
[VAR_NAME]= # Needed in: [Convex only]
# App
[VAR_NAME]= # Needed in: [Next.js only]Want the AI to generate a project-specific CLAUDE.md? Try Build a Startup →