Vibestacks LogoVibestacks
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

FilePurpose
code-style.mdTypeScript conventions, imports, naming
components.mdReact patterns, Server vs Client components
database.mdDrizzle ORM queries, schema, migrations
api.mdRoute handlers, validation, error handling
auth.mdBetter Auth patterns
stripe.mdSubscription 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"; // Avoid

Naming 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 primitivescomponents/ui/button.tsx
  • Page sectionsblocks/hero/hero-simple.tsx
  • Feature componentscomponents/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 here

Rules 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.