AI Coding
Coding Rules
Pre-configured coding rules that ensure Claude generates code following your project's patterns and conventions.
Vibestacks includes modular coding rules in .claude/rules/ that ensure Claude generates code matching your project's conventions. These rules are automatically loaded when Claude Code starts.
Rule Files
| File | Purpose |
|---|---|
code-style.md | TypeScript conventions, imports, naming |
components.md | React patterns, Server vs Client components |
database.md | Drizzle ORM queries, schema, migrations |
api.md | Route handlers, validation, error handling |
auth.md | Better Auth patterns |
stripe.md | Subscription and payment handling |
Code Style Rules
TypeScript
// Use strict mode - no `any` unless absolutely necessary
function processUser(user: User) { /* ... */ } // Good
function processUser(user: any) { /* ... */ } // Avoid
// Prefer interface for objects, type for unions
interface User {
id: string;
name: string;
}
type Status = "active" | "pending" | "inactive";
// Use explicit return types for exported functions
export function getUser(id: string): Promise<User | null> {
// ...
}Imports
// Order: React → External → Internal → Relative → Types
import { useState } from "react";
import { z } from "zod";
import { db } from "@/db";
import { Button } from "@/components/ui/button";
import { formatDate } from "./utils";
import type { User } from "@/types";
// Use path aliases (@/) instead of relative paths
import { auth } from "@/lib/auth"; // Good
import { auth } from "../../../lib/auth"; // AvoidNaming Conventions
// PascalCase for components and types
function UserProfile() { }
interface UserSettings { }
// camelCase for functions and variables
const userName = "John";
function getUserById(id: string) { }
// SCREAMING_SNAKE_CASE for constants
const MAX_RETRIES = 3;
const API_TIMEOUT = 5000;Component Rules
Server vs Client Components
// Default to Server Components (no directive needed)
export default function UserList() {
const users = await db.select().from(user);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Add 'use client' only when needed:
// - React hooks (useState, useEffect)
// - Browser APIs (localStorage, window)
// - Event handlers (onClick, onChange)
"use client";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Component Structure
// Props interface above component
interface ProfileCardProps {
user: User;
showActions?: boolean;
}
// Accept className for customization
export function ProfileCard({ user, showActions = true, className }: ProfileCardProps) {
return (
<Card className={cn("p-4", className)}>
{/* content */}
</Card>
);
}File Locations
- UI primitives →
components/ui/button.tsx - Page sections →
blocks/hero/hero-simple.tsx - Feature components →
components/dashboard/stats-card.tsx
Database Rules
Schema Definition
// Use text IDs with nanoid, not auto-increment
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
export const project = pgTable("project", {
id: text("id").primaryKey().$defaultFn(() => nanoid()),
name: text("name").notNull(),
userId: text("user_id").notNull().references(() => user.id, {
onDelete: "cascade"
}),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});Queries
// Use query builder, not raw SQL
const projects = await db
.select()
.from(project)
.where(eq(project.userId, userId))
.orderBy(desc(project.createdAt));
// Use transactions for multi-step operations
await db.transaction(async (tx) => {
await tx.insert(project).values({ name, userId });
await tx.insert(projectMember).values({ projectId, userId, role: "owner" });
});API Route Rules
Structure
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { z } from "zod";
// Validation schema at the top
const createProjectSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
});
export async function POST(request: NextRequest) {
// 1. Check authentication
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 2. Validate input
const body = await request.json();
const result = createProjectSchema.safeParse(body);
if (!result.success) {
return NextResponse.json({ error: result.error.flatten() }, { status: 400 });
}
// 3. Perform operation
const project = await db.insert(projects).values({
...result.data,
userId: session.user.id,
}).returning();
// 4. Return response
return NextResponse.json(project[0], { status: 201 });
}Error Responses
// Use consistent error format
return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
return NextResponse.json({ error: "Validation failed", details: errors }, { status: 400 });Authentication Rules
Server-Side
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
// In Server Components or Route Handlers
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
redirect("/sign-in");
}
// Access user data
const userId = session.user.id;
const userEmail = session.user.email;Client-Side
"use client";
import { authClient } from "@/lib/auth-client";
// Get session
const { data: session } = authClient.useSession();
// Sign out
await authClient.signOut();
// Sign in
await authClient.signIn.email({ email, password });Stripe Rules
Check Subscription
import { auth } from "@/lib/auth";
import { getPlan } from "@/config/app";
const session = await auth.api.getSession({ headers: await headers() });
const subscription = session?.user?.subscription;
// Check if user has active subscription
const isSubscribed = subscription?.status === "active";
// Get plan details
const plan = getPlan(subscription?.planId);
const canAccessFeature = plan?.limits?.features?.includes("advanced-analytics");Feature Gating
// In API routes
if (!isSubscribed) {
return NextResponse.json(
{ error: "Upgrade required", code: "SUBSCRIPTION_REQUIRED" },
{ status: 403 }
);
}
// In components
{isSubscribed ? (
<AdvancedFeature />
) : (
<UpgradePrompt />
)}Customizing Rules
You can modify any rule file in .claude/rules/ to match your preferences:
<!-- .claude/rules/code-style.md -->
# Code Style
## TypeScript
- Always use semicolons (or never - your choice)
- Prefer arrow functions for callbacks
- ...your rules hereRules Are Suggestions
Claude follows these rules as guidelines. If you ask for something specific that contradicts a rule, Claude will follow your explicit instructions.
Adding Custom Rules
Create new rule files for project-specific patterns:
# Create a rule for your domain
touch .claude/rules/billing.md<!-- .claude/rules/billing.md -->
# Billing Rules
## Price Display
- Always show prices in cents internally (1900 = $19.00)
- Use formatPrice() helper for display
- Include currency symbol from user's locale
## Invoice Generation
- Use the InvoiceTemplate component
- Include line items, tax, and total
- ...Claude will automatically load the new rule file.