Files
condado-newsletter/CLAUDE.md
Gabriel Sancho 11f80b9dd7 docs(policy): enforce server-side data ownership and backend LLM mediation
- clarify frontend may only rely on backend-issued session token cookie for auth

- forbid frontend browser storage for domain/business data

- require backend-mediated LLM calls across agent workflows
2026-03-27 02:49:16 -03:00

30 KiB

Condado Abaixo da Média SA — Email Bot

A monorepo containing a Kotlin/Spring Boot backend and a React frontend. This file gives the AI persistent instructions and context about the project so every session starts with the right knowledge.


Default Workflow: Test-Driven Development (TDD)

Every implementation step in this project follows TDD. This is non-negotiable.

The cycle for every step is:

Phase Action Gate
Red Write test file(s) for the step. Run the test suite. New tests must fail.
Green Write the minimum implementation to make all tests pass. All tests must pass.
Refactor Clean up the implementation. Tests must stay green.
Done Mark step only when the full build is green. ./gradlew build / npm run build && npm run test

Rules:

  • Never write implementation code before the test file exists and the tests fail.
  • Never mark a step Done unless the full test suite passes.
  • Test method names (Kotlin/JUnit): should_[expectedBehavior]_when_[condition].
  • Backend mocking: MockK only (not Mockito).
  • Backend integration tests: @SpringBootTest with H2 in-memory database.
  • Frontend tests: Vitest + React Testing Library, mocked Axios.

Project Overview

  • Type: Monorepo (backend + frontend in the same repository)
  • Purpose: Simulate virtual employees of the fictional company "Condado Abaixo da Média SA". Each entity is an AI-powered fictional employee with a name, email address, job title, personality, and an email schedule. At the scheduled time, the system reads recent emails from the company mailbox (IMAP), builds a prompt, calls the OpenAI API, and dispatches the AI-generated email via SMTP.
  • Tone rule (critical): Every generated email must be written in an extremely formal, corporate tone — but the content is completely casual and nonsensical, like internal jokes between friends. This contrast is the core joke of the project and must be preserved in every generated email.
  • Architecture: React SPA → Spring Boot REST API → PostgreSQL + IMAP + SMTP + OpenAI
  • Deployment: Fully containerized with Docker and Docker Compose

Tech Stack

Backend

Layer Technology
Language Kotlin (JVM)
Framework Spring Boot 3.x
Build Tool Gradle (Kotlin DSL — build.gradle.kts)
Database PostgreSQL (via Spring Data JPA)
Email Reading Jakarta Mail (IMAP) — read inbox for context
Email Sending Spring Mail (SMTP / JavaMailSender)
AI Integration OpenAI API (gpt-4o) via Spring RestClient
Scheduler Spring @Scheduled + SchedulingConfigurer
Auth JWT — issued by backend on login
Testing JUnit 5 + MockK
Docs Springdoc OpenAPI (Swagger UI)

Frontend

Layer Technology
Language TypeScript
Framework React 18 + Vite
UI Library shadcn/ui (Radix UI + Tailwind CSS)
State React Query (TanStack Query v5)
Routing React Router v6
HTTP Client Axios
Auth JWT stored in httpOnly cookie (set by backend)
Testing Vitest + React Testing Library

Infrastructure

Layer Technology
Containers Docker
Orchestration Docker Compose — three flavours (see below)
Reverse Proxy Nginx (serves frontend + proxies /api to backend)
Dev Mail Mailhog (SMTP trap + web UI)
All-in-one image Single Docker image: Nginx + Spring Boot + PostgreSQL + Supervisor
Image registry Docker Hub (<dockerhub-user>/condado-newsletter)
CI/CD GitHub Actions — build, test, push to Docker Hub on merge to main

Deployment Flavours

There are three ways to run the project:

Flavour Command When to use
Dev docker compose up Local development — includes Mailhog
Prod (compose) docker compose -f docker-compose.prod.yml up Production with external DB/SMTP
All-in-one docker run ... Simplest deploy — everything in one container

All-in-one Image

The all-in-one image (Dockerfile.allinone) bundles everything into a single container:

  • Nginx — serves the React SPA and proxies /api to Spring Boot
  • Spring Boot — the backend API + scheduler
  • PostgreSQL — embedded database
  • Supervisor — process manager that starts and supervises all three processes

This image is published to Docker Hub at <dockerhub-user>/condado-newsletter:latest.

Minimal docker run command:

docker run -d \
  -p 80:80 \
  -e APP_PASSWORD=yourpassword \
  -e JWT_SECRET=yoursecret \
  -e OPENAI_API_KEY=sk-... \
  -e MAIL_HOST=smtp.example.com \
  -e MAIL_PORT=587 \
  -e MAIL_USERNAME=company@example.com \
  -e MAIL_PASSWORD=secret \
  -e IMAP_HOST=imap.example.com \
  -e IMAP_PORT=993 \
  -e APP_RECIPIENTS=friend1@example.com,friend2@example.com \
  -v condado-data:/var/lib/postgresql/data \
  <dockerhub-user>/condado-newsletter:latest

The app is then available at http://localhost.


System Topology

Multi-container (Docker Compose)

┌─────────────────────────────────────────────────────────────────┐
│                        Docker Compose                           │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    nginx  :80 / :443                     │  │
│  │   • Serves React SPA (static files)                      │  │
│  │   • Proxies /api/**  ──────────────────────────────────► │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                  backend  :8080                          │  │
│  │   Spring Boot (Kotlin)                                   │  │
│  │   • REST API  /api/v1/**                                 │  │
│  │   • JWT auth  /api/auth/login                            │  │
│  │   • Swagger   /swagger-ui.html                           │  │
│  │   • Scheduler (cron per VirtualEntity)                   │  │
│  └──────────┬───────────────┬──────────────────────────────┘  │
│             │               │                                   │
│             ▼               ▼                                   │
│  ┌──────────────┐  ┌────────────────────────────────────────┐  │
│  │ postgres :5432│  │  External services (outside Docker)    │  │
│  │ PostgreSQL   │  │  • OpenAI API  (HTTPS)                  │  │
│  │ DB: condado  │  │  • SMTP server (send emails)            │  │
│  └──────────────┘  │  • IMAP server (read inbox)             │  │
│                    └────────────────────────────────────────┘  │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │            mailhog  :1025 (SMTP) / :8025 (UI)            │  │
│  │            DEV ONLY — catches outgoing emails            │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Browser  ──────►  nginx :80
                  ├── /        → React SPA (index.html)
                  ├── /assets  → Static JS/CSS
                  └── /api/**  → backend :8080

All-in-one Image (single docker run)

┌──────────────────────────────────────────────────────────────┐
│            condado-newsletter  :80  (single container)       │
│                                                              │
│   supervisord                                                │
│       ├── nginx          (port 80 — SPA + /api proxy)        │
│       ├── java -jar app  (Spring Boot :8080 — internal only) │
│       └── postgres       (PostgreSQL :5432 — internal only)  │
│                                                              │
│   Persistent data → Docker volume mounted at                 │
│       /var/lib/postgresql/data                               │
│                                                              │
│   External (via env vars):                                   │
│       • OpenAI API  (HTTPS)                                  │
│       • SMTP server (send emails)                            │
│       • IMAP server (read inbox)                             │
└──────────────────────────────────────────────────────────────┘

Data flows:

  1. User opens browser → Nginx serves the React app.
  2. React calls POST /api/auth/login with password → backend validates against APP_PASSWORD env var → returns JWT in httpOnly cookie.
  3. React calls /api/v1/virtual-entities (JWT cookie sent automatically) → backend responds.
  4. Scheduler ticks → for each due VirtualEntity:
    • Backend reads IMAP inbox (external IMAP server).
    • Builds prompt → calls OpenAI API.
    • Sends generated email via SMTP (Mailhog in dev, real SMTP in prod).
    • Saves DispatchLog to PostgreSQL.

Monorepo Structure

condado-news-letter/               ← repo root
├── CLAUDE.md
├── INSTRUCTIONS.md
├── .env.example                   ← template for all env vars
├── .gitignore
├── docker-compose.yml             ← dev stack (Nginx + Backend + PostgreSQL + Mailhog)
├── docker-compose.prod.yml        ← prod stack (Nginx + Backend + PostgreSQL)
├── Dockerfile.allinone            ← all-in-one image (Nginx + Backend + PostgreSQL + Supervisor)
│
├── .github/
│   └── workflows/
│       ├── ci.yml                 ← run tests on every PR
│       └── publish.yml            ← build & push all-in-one image to Docker Hub on main merge
│
├── backend/                       ← Spring Boot (Kotlin + Gradle)
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   ├── Dockerfile
│   └── src/
│       ├── main/kotlin/com/condado/newsletter/
│       │   ├── CondadoApplication.kt
│       │   ├── config/
│       │   ├── controller/
│       │   ├── service/
│       │   │   ├── AuthService.kt
│       │   │   ├── EntityService.kt
│       │   │   ├── EmailReaderService.kt
│       │   │   ├── PromptBuilderService.kt
│       │   │   ├── AiService.kt
│       │   │   └── EmailSenderService.kt
│       │   ├── repository/
│       │   ├── model/
│       │   ├── dto/
│       │   └── scheduler/
│       └── main/resources/
│           ├── application.yml
│           └── application-dev.yml
│
├── frontend/                      ← React (TypeScript + Vite)
│   ├── package.json
│   ├── vite.config.ts
│   ├── tsconfig.json
│   ├── Dockerfile
│   └── src/
│       ├── main.tsx
│       ├── App.tsx
│       ├── api/                   ← Axios client + React Query hooks
│       ├── components/            ← Reusable UI components (shadcn/ui)
│       ├── pages/
│       │   ├── LoginPage.tsx
│       │   ├── DashboardPage.tsx
│       │   ├── EntitiesPage.tsx
│       │   └── LogsPage.tsx
│       └── router/                ← React Router config
│
└── nginx/
    └── nginx.conf                 ← Shared Nginx config (used in both Docker flavours)

Build & Run Commands

Backend

cd backend

# Build the project
./gradlew build

# Run (dev profile)
./gradlew bootRun --args='--spring.profiles.active=dev'

# Run tests
./gradlew test

# Run a specific test
./gradlew test --tests "com.condado.newsletter.service.PromptBuilderServiceTest"

Frontend

cd frontend

# Install dependencies
npm install

# Dev server (with Vite proxy to backend)
npm run dev

# Build for production
npm run build

# Run tests
npm run test

Full Stack (Docker Compose)

# Dev (Mailhog included)
docker compose up --build

# Prod
docker compose -f docker-compose.prod.yml up --build

# Stop
docker compose down

Coding Standards

Project Language Policy

  • Primary language is English for code, comments, commit messages, PR titles/descriptions, and user-facing UI text.
  • Portuguese may be used only for informal chat outside code artifacts.
  • When translating requirements to implementation, preserve meaning but keep identifiers and UI labels in English.

Backend (Kotlin)

  • Use Kotlin idiomatic style: data classes, extension functions, and null-safety operators.
  • Prefer val over var wherever possible.
  • Use constructor injection for dependencies (never field injection with @Autowired).
  • All DTOs must be data classes with validation annotations (jakarta.validation).
  • Controller methods must return ResponseEntity<T> with explicit HTTP status codes.
  • Services must be annotated with @Service and never depend on controllers.
  • Repositories must extend JpaRepository<Entity, IdType>.
  • Use @Transactional on service methods that modify data.
  • All public functions must have KDoc comments.
  • Use snake_case for database columns and camelCase for Kotlin properties.
  • Keep controllers thin — business logic belongs in services.
  • The AI prompt construction logic must live exclusively in PromptBuilderService.

Frontend (TypeScript / React)

  • All components must be functional — no class components.
  • Use TypeScript strict mode — no any types.
  • All API calls go through the src/api/ layer — never call axios directly in a component.
  • Use React Query for all server state — never store server data in useState.
  • Use shadcn/ui components for UI — do not build custom UI primitives from scratch.
  • All pages live in src/pages/ and are lazy-loaded via React Router.
  • Protected routes must check for a valid JWT before rendering.
  • No hardcoded strings that face the user — use constants or i18n keys.

Authentication Model

  • There is one single user — the admin (you and your friends sharing the app).
  • The password is set via the APP_PASSWORD environment variable on the backend.
  • POST /api/auth/login accepts { "password": "..." } and returns a JWT in an httpOnly cookie (no username needed).
  • The JWT is validated by Spring Security on every protected backend request.
  • The React frontend redirects to /login if the cookie is absent or the JWT is expired.
  • There is no user registration, no user table, no role system.

Data Ownership Policy (Critical)

  • All business data must be persisted server-side (PostgreSQL via backend APIs).
  • The frontend must treat the backend as the single source of truth for entities, tasks, generated preview messages/history, logs, and any other domain data.
  • The frontend must not persist business/domain data in browser storage (localStorage, sessionStorage, IndexedDB) or call LLM providers directly.
  • The only browser-stored auth state is the backend-issued session token cookie (httpOnly JWT).
  • If a required endpoint does not exist yet, implement it in the backend first; do not add frontend-side persistence workarounds.

Naming Conventions

Backend

Artifact Convention Example
Classes PascalCase PromptBuilderService
Functions camelCase buildPrompt()
Variables camelCase entityList
Constants SCREAMING_SNAKE_CASE MAX_EMAIL_CONTEXT_DAYS
DB tables snake_case (plural) virtual_entities
REST endpoints kebab-case /api/v1/virtual-entities
Packages lowercase com.condado.newsletter

Frontend

Artifact Convention Example
Components PascalCase EntityCard.tsx
Hooks camelCase + use useEntities.ts
API files camelCase entitiesApi.ts
Pages PascalCase + Page EntitiesPage.tsx
CSS classes kebab-case (Tailwind) text-sm font-medium

Testing Guidelines

This project follows strict TDD. For every implementation step, tests are written first (Red), then the implementation is added until all tests pass (Green), then code is cleaned up (Refactor). Never write implementation code before the tests exist and fail.

TDD Workflow (apply to every step)

  1. Red — Write the test file(s). Run ./gradlew test (backend) or npm run test (frontend) and confirm the new tests fail (compile errors are acceptable at this stage).
  2. Green — Write the minimum implementation needed to make all tests pass.
  3. Refactor — Clean up the implementation while keeping tests green.
  4. A step is only Done when ./gradlew build (backend) or npm run build && npm run test (frontend) is fully green.

Backend

  • Every service class must have a corresponding unit test class written before the service.
  • Use MockK for mocking (not Mockito).
  • Integration tests use @SpringBootTest and an H2 in-memory database.
  • Test method names follow: should_[expectedBehavior]_when_[condition].
  • Minimum 80% code coverage for service classes.
  • Test files live in src/test/kotlin/ mirroring the src/main/kotlin/ package structure.

Frontend

  • Every page component must have at least a smoke test (renders without crashing), written before the component.
  • API layer functions must be tested with mocked Axios responses.
  • Use Vitest as the test runner and React Testing Library for component tests.
  • Test files live in src/__tests__/ mirroring the src/ structure.

Environment Variables

All variables are defined in .env (root of the monorepo) and injected by Docker Compose. Never hardcode any of these values.

Variable Used by Description
APP_PASSWORD Backend Single admin password for login
JWT_SECRET Backend Secret key for signing/verifying JWTs
JWT_EXPIRATION_MS Backend JWT expiry in milliseconds (e.g. 86400000 = 1 day)
SPRING_DATASOURCE_URL Backend PostgreSQL connection URL
SPRING_DATASOURCE_USERNAME Backend DB username
SPRING_DATASOURCE_PASSWORD Backend DB password
MAIL_HOST Backend SMTP host (Mailhog in dev, real SMTP in prod)
MAIL_PORT Backend SMTP port
MAIL_USERNAME Backend SMTP username (also IMAP login)
MAIL_PASSWORD Backend SMTP/IMAP password
IMAP_HOST Backend IMAP host for reading the shared inbox
IMAP_PORT Backend IMAP port (default: 993)
IMAP_INBOX_FOLDER Backend IMAP folder to read (default: INBOX)
OPENAI_API_KEY Backend OpenAI API key
OPENAI_MODEL Backend OpenAI model (default: gpt-4o)
APP_RECIPIENTS Backend Comma-separated list of recipient emails
VITE_API_BASE_URL Frontend Backend API base URL (used by Vite at build time)

⚠️ Never hardcode credentials. Always use environment variables or a .env file (gitignored).


Key Domain Concepts

  • VirtualEntity: A fictional employee of "Condado Abaixo da Média SA". Has a name, a real email address (used as sender), a job title, a personality description, an email schedule (cron expression), and an email context window (how many days back to read emails for context).

  • EmailContext: A snapshot of recent emails read from the shared IMAP inbox, filtered by the entity's configured context window (e.g., last 3 days). Used to give the AI conversational context.

  • Prompt: The full text sent to the OpenAI API. Built by PromptBuilderService from the entity's profile + the EmailContext. Always instructs the AI to write in an extremely formal corporate tone with completely casual/nonsensical content.

  • DispatchLog: A record of each AI email generation and send attempt for a given entity. Stores the generated prompt, the AI response, send status, and timestamp.


The Prompt Template (Core Logic)

Every prompt sent to the AI must follow this structure:

You are [entity.name], [entity.jobTitle] at "Condado Abaixo da Média SA".

Your personality: [entity.personality]

IMPORTANT TONE RULE: You must write in an extremely formal, bureaucratic, corporate tone —
as if writing an official memo. However, the actual content of the email must be completely
casual, trivial, or nonsensical — as if talking to close friends about mundane things.
The contrast between the formal tone and the casual content is intentional and essential.

Here are the most recent emails from the company inbox (last [entity.contextWindowDays] days)
for context:

[list of recent emails: sender, subject, body]

Write a new email to be sent to the company group, continuing the conversation naturally.
Reply or react to the recent emails if relevant. Sign off as [entity.name], [entity.jobTitle].

Format your response exactly as:
SUBJECT: <subject line here>
BODY:
<full email body here>

Task Preview Generation Rules

  • The frontend must never call LLM providers (OpenAI/Ollama/Llama) directly.
  • The frontend requests backend endpoints only.
  • The backend is responsible for:
    • building the final prompt,
    • calling the configured LLM endpoint,
    • returning the generated message to the frontend.
  • Generated preview message history must be persisted in the backend database (not browser storage), so history survives reloads and restarts.

Git Workflow & CI/CD

  • Branch naming: feature/<short-description>, fix/<short-description>, chore/<short-description>
  • Commit messages follow Conventional Commits: feat:, fix:, chore:, docs:, test:
  • Scope your commits: feat(backend):, feat(frontend):, chore(docker):
  • TDD commit order per step: first test(<scope>): add failing tests for <step>, then feat(<scope>): implement <step> — all tests passing.
  • PRs require all CI checks to pass before merging.
  • Never commit directly to main.

Commit Rules (enforced by AI)

These rules apply to every commit made during AI-assisted implementation:

Rule Detail
Two commits per TDD step 1st commit = failing tests (Red), 2nd commit = passing implementation (Green)
Commit after each step Never accumulate multiple steps in one commit
Red commit subject test(<scope>): add failing tests for step <N> — <short description>
Green commit subject feat(<scope>): implement step <N> — <short description>
Scope values backend, frontend, docker, ci, config
Body Optional but encouraged: list what was added/changed
No --no-verify Never bypass git hooks
No force push Never use --force on shared branches
Atomic commits Each commit must leave the build green (except deliberate Red-phase test commits)
chore for housekeeping Config changes, dependency tweaks, file renames → chore(<scope>):
fix for bug fixes fix(<scope>): <what was broken and how it was fixed>
docs for documentation Changes to CLAUDE.md, INSTRUCTIONS.md, README.mddocs:

Meaningful Commit Quality Bar

Every commit must be understandable without opening the full diff.

  • Subject line must explain intent + scope + outcome.
  • Avoid vague subjects like update frontend, fix stuff, changes, wip.
  • Keep commits focused on one concern (tests, feature, refactor, docs), not mixed changes.
  • Prefer small atomic commits that can be reverted safely.
  • Include a commit body when context is not obvious (what changed, why, risks).

Good examples:

  • test(frontend): add failing tests for step 2 - entity task detail flow
  • feat(frontend): implement step 2 - per-entity scheduled task creation
  • docs(config): clarify english-first language policy and commit quality rules

GitHub Actions Workflows

Workflow file Trigger What it does
.github/workflows/ci.yml Push / PR to any branch Backend tests (./gradlew test) + Frontend tests (npm run test)
.github/workflows/publish.yml Push to main Builds Dockerfile.allinone, tags as latest + git SHA, pushes to Docker Hub

Required GitHub Secrets:

Secret Description
DOCKERHUB_USERNAME Docker Hub account username
DOCKERHUB_TOKEN Docker Hub access token (not password)

Image tags pushed on every main merge:

  • <dockerhub-user>/condado-newsletter:latest
  • <dockerhub-user>/condado-newsletter:<git-sha> (for pinning)

Step 1 Decisions & Versions

Decision Detail
Gradle wrapper 8.14.1 (upgraded from 8.7 — Gradle < 8.14 cannot parse Java version 26)
Spring Boot 3.4.5 (latest stable at time of scaffold)
Kotlin 2.1.21 (latest stable, bundled with Gradle 8.14.1)
Java toolchain 21 configured in build.gradle.kts via kotlin { jvmToolchain(21) } — bytecode targets Java 21 regardless of host JDK
Frontend test script vitest run --passWithNoTests — prevents CI failure before Step 12 adds real tests