ignitionstack.pro v1.0 is out! Read the announcement →
Skip to Content

AI Agents Guide - ignitionstack.pro Boilerplate

🤖 Overview

This document describes how AI agents (Claude Code, Cursor, Windsurf, Copilot, etc.) should interact with this codebase to maintain consistency and quality.

📚 Project Architecture

Primary Stack

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)

Architectural Patterns

  1. Repository Pattern: Centralized data access
  2. Clean Architecture: Separation of concerns
  3. SOLID Principles: Maintainable and extensible code
  4. ActionResult Pattern: Typed error handling

🎯 Guidelines for AI Agents

1. Understand the Context

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 flow

2. Reading the Code

When 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 types

Import 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";

3. Writing Code

Pattern 1: Repository Pattern (MANDATORY)

// ✅ 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();

Pattern 2: Structured Logging (MANDATORY)

// ✅ 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);

Pattern 3: Type Assertions (Supabase)

// ✅ 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 };

Pattern 4: ActionResult (Server Actions)

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"); }

4. Creating New Features

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)

5. Modifying Existing Code

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!

6. Debugging and Troubleshooting

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

🔒 Security Rules

ALWAYS ensure:

// 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"

🌍 Internationalization (i18n)

ALWAYS add translations:

// 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>;

📊 Performance

Mandatory optimizations:

// 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} />

🧪 Testing

Test pattern:

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); }); }); });

📝 Documentation

When to document:

/** * 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

Files to update:

  1. IMPROVEMENTS.md - Technical changes
  2. README.md - Se user-facing
  3. Guides em docs/ - Se nova feature
  4. AGENTS.md - When adding a new pattern

⚡ Quick Reference

Common commands

# 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 check

Pull Request checklist

🎯 Goals for AI Agents

  1. Maintain Consistency: Follow existing patterns rigorously
  2. Quality over Speed: Well-structured code > rushed code
  3. Security First: RLS, validation, and logging everywhere
  4. Document Changes: Update the relevant docs
  5. Test Thoroughly: Cover happy and unhappy paths

🚫 Absolute Prohibitions

  1. ❌ Console.log/error/warn (use Pino logger)
  2. ❌ Direct Supabase queries outside repositories
  3. ❌ Hardcoded strings (use i18n)
  4. ❌ Secrets hardcoded in the code
  5. ❌ Imports relativos (use @/ alias)
  6. ❌ Skipping input validation
  7. ❌ Skipping error handling
  8. ❌ Ignoring RLS policies
  9. ❌ Mixing server/client code
  10. ❌ Deploying without testing migrations

📚 Recursos Adicionais

Project documentation

Specific rules

External documentation


Version: 2.0 Last Update: November 17, 2025 Status: Supabase migration complete ✅

Keep this pattern. Keep the quality. Keep the consistency.