This document describes how AI agents (Claude Code, Cursor, Windsurf, Copilot, etc.) should interact with this codebase to maintain consistency and quality.
Next.js 15.2.0 (App Router)
├── TypeScript 5 (strict mode)
├── Supabase (PostgreSQL + Auth)
├── Tailwind CSS 4.0.9
├── Zod 3.24.1 (validation)
├── Pino (structured logging)
└── next-intl (i18n: pt, en, es)Before making changes, ALWAYS:
// 1. Identify the architecture layer
// - Presentation (components/)
// - Application (actions/, server/)
// - Domain (types/, validations/)
// - Infrastructure (repositories/, lib/supabase/)
// 2. Check existing patterns
// Look for similar files and follow the same pattern
// 3. Understand the dependencies
// Inspect imports/exports to understand data flowWhen reviewing code, focus on:
Folder Structure
src/app/
├── [locale]/ # i18n routes (ALWAYS use)
├── actions/ # Server Actions (mutations)
├── server/ # Server Queries (reads)
├── lib/
│ ├── repositories/ # Data access (ALWAYS use)
│ ├── supabase/ # Clients (server, admin)
│ ├── validations/ # Zod schemas
│ └── logger.ts # Structured logging
├── components/ # React components
└── types/ # TypeScript typesImport Patterns
// ✅ ALWAYS use absolute imports with @/
import { Button } from "@/app/components/ui/button";
import { PostRepository } from "@/app/lib/repositories";
// ❌ NEVER use relative imports
import { Button } from "../../components/ui/button";// ✅ CORRECT - Use the repository
import { PostRepository } from "@/app/lib/repositories";
export async function getPost(id: string) {
return await PostRepository.findById(id);
}
// ❌ WRONG - Direct Supabase query
const { data } = await supabase.from("posts").select();// ✅ CORRECT - Use the Pino logger
import { createServiceLogger } from "@/app/lib/logger";
const logger = createServiceLogger("service-name");
try {
const result = await operation();
logger.info({ context }, "Operation successful");
} catch (error) {
logger.error({ error, context }, "Operation failed");
}
// ❌ ERRADO - Console methods
console.log("Debug");
console.error("Error:", error);// ✅ CORRECT - Type assertions for Supabase
const { data, error } = await supabase
.from("table")
.insert(data as unknown as never) // Type assertion
.select()
.single();
const row = data as unknown as Record<string, unknown>;
// Map snake_case → camelCase
return {
userId: row.user_id as string,
createdAt: row.created_at as string
};import type { ActionResult } from "@/app/lib/action-result";
// ✅ CORRECT - Return an ActionResult
export async function createPost(
data: CreatePostInput
): Promise<ActionResult<{ id: string }>> {
try {
const result = await PostRepository.create(data);
return { success: true, data: { id: result.id } };
} catch (error) {
logger.error({ error }, "Failed to create post");
return {
success: false,
error: "Failed to create post",
code: "CREATE_FAILED"
};
}
}
// ❌ WRONG - Throw errors
export async function createPost(data: CreatePostInput) {
throw new Error("Something went wrong");
}Follow this order EVERY time:
// 1️⃣ TYPES (src/app/types/)
export interface Post {
id: string;
title: string;
userId: string;
createdAt: string;
}
export interface CreatePostInput {
title: string;
content: string;
}
// 2️⃣ VALIDATION (src/app/lib/validations/)
import { z } from "zod";
export const createPostSchema = z.object({
title: z.string().min(10).max(200),
content: z.string().min(50)
});
// 3️⃣ MIGRATION (supabase/migrations/NNN_description.sql)
CREATE TABLE IF NOT EXISTS public.posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title TEXT NOT NULL,
user_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user_id ON public.posts(user_id);
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read own posts"
ON public.posts FOR SELECT
USING (auth.uid() = user_id);
// 4️⃣ REPOSITORY (src/app/lib/repositories/)
import { createServiceLogger } from "@/app/lib/logger";
const logger = createServiceLogger("posts");
export const PostRepository = {
async findById(id: string): Promise<Post | null> {
try {
const supabase = await createClient();
const { data, error } = await supabase
.from("posts")
.select("*")
.eq("id", id)
.maybeSingle();
if (error) {
logger.error({ error, id }, "Failed to find post");
return null;
}
return mapDbToPost(data as Record<string, unknown>);
} catch (error) {
logger.error({ error, id }, "Unexpected error");
return null;
}
},
async create(data: CreatePostInput): Promise<ActionResult<{ id: string }>> {
try {
const supabase = await createClient();
const { data: post, error } = await supabase
.from("posts")
.insert(data as unknown as never)
.select("id")
.single();
if (error) {
logger.error({ error, data }, "Failed to create post");
return { success: false, error: "Creation failed", code: "CREATE_FAILED" };
}
return { success: true, data: { id: post.id } };
} catch (error) {
logger.error({ error, data }, "Unexpected error");
return { success: false, error: "Unexpected error", code: "UNEXPECTED_ERROR" };
}
}
};
function mapDbToPost(data: Record<string, unknown>): Post {
return {
id: data.id as string,
title: data.title as string,
userId: data.user_id as string,
createdAt: data.created_at as string
};
}
// 5️⃣ SERVER QUERY (src/app/server/posts/get-post-by-id.ts)
import { PostRepository } from "@/app/lib/repositories";
export async function getPostById(id: string): Promise<Post | null> {
return await PostRepository.findById(id);
}
// 6️⃣ SERVER ACTION (src/app/actions/posts/create-post.ts)
"use server";
import { PostRepository } from "@/app/lib/repositories";
import { createPostSchema } from "@/app/lib/validations/posts";
import type { ActionResult } from "@/app/lib/action-result";
export async function createPost(
formData: FormData
): Promise<ActionResult<{ id: string }>> {
const data = createPostSchema.parse({
title: formData.get("title"),
content: formData.get("content")
});
return await PostRepository.create(data);
}
// 7️⃣ COMPONENT (src/app/components/posts/post-form.tsx)
"use client";
import { useTranslations } from "next-intl";
import { createPost } from "@/app/actions/posts/create-post";
export function PostForm() {
const t = useTranslations("Blog");
async function handleSubmit(formData: FormData) {
const result = await createPost(formData);
if (result.success) {
// Handle success
} else {
// Handle error
}
}
return (
<form action={handleSubmit}>
<input name="title" placeholder={t("titlePlaceholder")} />
<button type="submit">{t("submit")}</button>
</form>
);
}
// 8️⃣ TRANSLATIONS (messages/pt.json, en.json, es.json)
{
"Blog": {
"titlePlaceholder": "Type the title",
"submit": "Publicar"
}
}
// 9️⃣ TESTS (src/app/lib/repositories/post-repository.test.ts)
describe("PostRepository", () => {
describe("findById", () => {
it("should return post when found", async () => {
const post = await PostRepository.findById("123");
expect(post).toBeDefined();
});
});
});
// 🔟 DOCUMENTATION (Update IMPROVEMENTS.md)When you touch an existing file:
// 1. Read the entire file BEFORE changing it
// 2. Identify which pattern it already uses
// 3. Follow the SAME pattern
// 4. Do NOT mix different patterns in the same file
// ✅ CORRECT - Follow the existing style
// If the file uses createServiceLogger, keep using it
const logger = createServiceLogger("service-name");
// ❌ WRONG - Mixing patterns
// If the file already uses the logger, do not add console.log
console.log("Debug"); // WRONG!When investigating issues:
// 1. Check the structured logs in the terminal
// Look for JSON entries with level: "ERROR"
// 2. Check RLS policies in Supabase
// SELECT * FROM pg_policies WHERE schemaname = 'public';
// 3. Confirm environment variables
// Make sure .env.local is configured
// 4. Inspect applied migrations
// npx supabase migration list
// 5. Regenerate types if needed
// npm run db:gen// 1. RLS enabled on EVERY table
ALTER TABLE public.table_name ENABLE ROW LEVEL SECURITY;
// 2. Validate input with Zod
const validated = schema.parse(userInput);
// 3. Use the correct client
// Regular (RLS): await createClient()
// Admin (bypass RLS): createAdminClient()
// 4. NUNCA exponha secrets
// ✅ process.env.SECRET_KEY (server)
// ✅ process.env.NEXT_PUBLIC_KEY (client-safe)
// ❌ const key = "hardcoded_secret"// 1. Add the key to messages/pt.json (base)
{
"Blog": {
"newKey": "New text in Portuguese"
}
}
// 2. Add it to messages/en.json
{
"Blog": {
"newKey": "New text in english"
}
}
// 3. Add it to messages/es.json
{
"Blog": {
"newKey": "New text in Spanish"
}
}
// 4. Use it in the component
const t = useTranslations("Blog");
return <p>{t("newKey")}</p>;// 1. Use Server Components by default
// Add "use client" only when NECESSARY
// 2. Use caching com tags
import { unstable_cache } from "next/cache";
export const getData = unstable_cache(
async () => { /* ... */ },
["cache-key"],
{ tags: ["data"], revalidate: 3600 }
);
// 3. Invalidate caches when mutating data
import { revalidateTag } from "next/cache";
await createData();
revalidateTag("data");
// 4. Optimize images
import Image from "next/image";
<Image src={url} alt="..." width={800} height={400} />import { describe, it, expect, beforeEach } from "@jest/globals";
describe("Feature/Component", () => {
describe("specific functionality", () => {
beforeEach(() => {
// Setup
});
it("should do something specific", () => {
// Arrange
const input = setupTestData();
// Act
const result = doSomething(input);
// Assert
expect(result).toBe(expected);
});
});
});/**
* JSDoc for public/exported functions
*
* @param id - Description
* @returns Description
*
* @example
* ```typescript
* const result = await function(id);
* ```
*/
export async function function(id: string) { }
// Only add inline comments when it is NOT obvious
// Avoid comments that simply restate the code
const user = await getUser(id); // Gets user - UNNECESSARY# Development
npm run dev # Dev server
npm run build # Production build
npm test # Run tests
# Database
npm run db:gen # Generate types
npx supabase db reset # Reset local DB
npx supabase db push # Push migrations
# Quality
npm run lint # ESLint
npx tsc --noEmit # Type checkVersion: 2.0 Last Update: November 17, 2025 Status: Supabase migration complete ✅
Keep this pattern. Keep the quality. Keep the consistency.