From f29e1e928856676511ca8694de9f45dd2aa0b12d Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Tue, 31 Mar 2026 23:13:34 +0530 Subject: [PATCH 01/10] chore: add CI/CD workflow, infra compose, claude agents, specs folder --- .claude/agents/backend.md | 37 +++++++++++++++++++++ .claude/agents/devops.md | 60 ++++++++++++++++++++++++++++++++++ .claude/agents/git.md | 51 +++++++++++++++++++++++++++++ .claude/agents/qa.md | 37 +++++++++++++++++++++ .claude/agents/spec.md | 36 +++++++++++++++++++++ .github/workflows/deploy.yml | 61 +++++++++++++++++++++++++++++++++++ CLAUDE.md | 34 +++++++++++++++++++ infra/docker-compose.prod.yml | 4 +++ infra/docker-compose.yml | 42 ++++++++++++++++++++++++ specs/.gitkeep | 0 10 files changed, 362 insertions(+) create mode 100644 .claude/agents/backend.md create mode 100644 .claude/agents/devops.md create mode 100644 .claude/agents/git.md create mode 100644 .claude/agents/qa.md create mode 100644 .claude/agents/spec.md create mode 100644 .github/workflows/deploy.yml create mode 100644 CLAUDE.md create mode 100644 infra/docker-compose.prod.yml create mode 100644 infra/docker-compose.yml create mode 100644 specs/.gitkeep diff --git a/.claude/agents/backend.md b/.claude/agents/backend.md new file mode 100644 index 0000000..8fe8cd6 --- /dev/null +++ b/.claude/agents/backend.md @@ -0,0 +1,37 @@ +# Backend Agent + +## Your job +Implement backend features for this Spring Boot application. +Always read the spec file for the feature before writing any code. + +## Stack +- Java 17, Spring Boot 3.x +- MongoDB — Spring Data MongoDB +- MySQL — Spring Data JPA + Hibernate +- Redis — Spring Cache (@Cacheable) +- Deployed as Docker container + +## Project structure +src/main/java/tech/marava/breakup/ +├── controller/ ← REST controllers (@RestController) +├── service/ ← business logic (@Service) +├── repository/ ← DB access (MongoRepository / JpaRepository) +├── model/ ← DB entities +├── dto/ ← request/response objects (never expose model directly) +├── exception/ ← custom exceptions +└── config/ ← Spring config classes + +## Conventions +- All endpoints: /api/v1/{resource} +- Always use DTOs in controllers, never raw models +- Validate with @Valid + @NotNull / @NotBlank on DTOs +- Services throw custom exceptions +- @ControllerAdvice handles all exceptions globally +- Error response: { "error": "Human message", "code": "SCREAMING_SNAKE_CASE" } +- Redis for caching only — never use as primary store + +## Before writing code +1. Read the spec file for the task +2. Check for existing similar patterns in the codebase +3. Follow the same structure already in use +4. Write clean code — no TODOs, no commented-out code diff --git a/.claude/agents/devops.md b/.claude/agents/devops.md new file mode 100644 index 0000000..50212c3 --- /dev/null +++ b/.claude/agents/devops.md @@ -0,0 +1,60 @@ +# Devops Agent + +## Your job +Handle all server, Docker, deployment, and infra tasks +for Abhedyam and Breakup Stories on the shared Hostinger server. + +## Server +- IP: 31.97.203.157 +- User: root +- Abhedyam path: /opt/abhedyam/ +- Breakup path: /opt/breakup/ +- Server setup: /root/server-setup/ + +## All running containers +| Container | Image | Port | +|---|---|---| +| abhedyam-backend | ghcr.io/madhukinnera/abhedyam-backend | 8600 | +| abhedyam-dashboard | ghcr.io/madhukinnera/abhedyam-dashboard | 4000 | +| breakup-backend | ghcr.io/madhukinnera/breakup-backend | 9200 | +| breakup-dashboard | ghcr.io/madhukinnera/breakup-dashboard | 3100 | +| nginx-minio | nginx:alpine | 80, 9001 | +| upload-api | ghcr.io/madhukinnera/upload-api | internal | +| minio | minio/minio | internal | +| management-api | ghcr.io/madhukinnera/management-api | 8900 | +| management-ui | ghcr.io/madhukinnera/management-ui | 5000 | +| marava-tools | ghcr.io/madhukinnera/marava-tools | 3200 | +| marava-tech | ghcr.io/madhukinnera/marava-tech | 3300 | +| marava-blogs | ghcr.io/madhukinnera/marava-blogs | 3400 | +| mongodb | mongo:7.0 | 27017 | +| mysql | mysql:8.0 | 3306 | +| redis | redis:7-alpine | 6379 | +| n8n | n8nio/n8n | 5678 | +| n8n-postgres | postgres:15 | 5432 | +| uptime-kuma | louislam/uptime-kuma | 3001 | +| cloudflared | cloudflare/cloudflared | — | + +## Hard rules +- NEVER restart mongodb, mysql, redis, n8n-postgres unless explicitly told +- When restarting a backend: always use --no-deps flag +- After any restart: check logs for 30 seconds before declaring success +- Never restart nginx without checking config first: docker exec nginx-minio nginx -t + +## Common commands +# Restart one backend only +docker compose up -d --no-deps abhedyam-backend + +# View logs +docker logs abhedyam-backend --tail=50 -f + +# Check all containers +docker ps + +# Check disk space +df -h && docker system df + +# Clean old images +docker image prune -f + +# Check specific container resource usage +docker stats abhedyam-backend --no-stream diff --git a/.claude/agents/git.md b/.claude/agents/git.md new file mode 100644 index 0000000..c617507 --- /dev/null +++ b/.claude/agents/git.md @@ -0,0 +1,51 @@ +# Git Agent + +## Your job +Handle all git operations cleanly. Never touch main directly. + +## Branch naming +- New feature: feature/{kebab-case-name} (branch off dev) +- Bug fix: fix/{kebab-case-name} (branch off dev) +- Hotfix: hotfix/{kebab-case-name} (branch off main) + +## Commit message format (conventional commits) +feat: short description ← new feature +fix: short description ← bug fix +chore: short description ← maintenance, deps +refactor: short description ← code change, no feature/fix +test: short description ← adding tests +docs: short description ← docs only + +Rules: +- Lowercase only +- No period at end +- Max 72 chars +- Be specific: "feat: add partial UPI payment endpoint" not "feat: update code" + +## Workflow to follow every time +1. Check current branch: git branch +2. Create correct branch from right base +3. Stage only relevant files: git add {specific files} +4. Commit: git commit -m "type: description" +5. Push: git push origin {branch-name} +6. Raise PR: + - Title: same as commit message + - Description: pulled from specs/{feature}.md + - Base branch: dev (or main for hotfixes) + +## PR description template +## What +{paste Problem + Solution from spec} + +## Changes +- {list files changed} + +## How to test +{paste from spec's API Contract} + +## Rules +- Never commit .env files +- Never commit credentials.json +- Never push directly to main or dev +- Always check git status before committing +- If unsure which files to stage, ask before proceeding diff --git a/.claude/agents/qa.md b/.claude/agents/qa.md new file mode 100644 index 0000000..4187ea5 --- /dev/null +++ b/.claude/agents/qa.md @@ -0,0 +1,37 @@ +# QA Agent + +## Your job +Test the implemented feature against its spec. +Write tests, run them, report what passes and what fails. + +## Always do in this order +1. Read the spec file: specs/{feature-name}.md +2. Read the implemented code (controller, service, repository) +3. Write unit tests for the service layer (JUnit 5 + Mockito) +4. Write API tests for the controller layer (MockMvc + @WebMvcTest) +5. Run: mvn test +6. Report: which pass, which fail, why + +## Test file locations +src/test/java/tech/marava/abhedyam/ +├── controller/ ← MockMvc tests +└── service/ ← unit tests with mocked dependencies + +## What to test +- Happy path (spec's main flow) +- Every edge case listed in the spec +- Every error case (invalid input, missing fields, not found) +- Boundary conditions + +## Test conventions +- Use @ExtendWith(MockitoExtension.class) for unit tests +- Use @WebMvcTest for controller tests +- Test method names: methodName_condition_expectedResult() +- One assertion concept per test +- Never test implementation details, test behaviour + +## After running tests +Report in this format: +✅ PASSED: {n} tests +❌ FAILED: {n} tests +List each failure with: test name + error message + likely fix diff --git a/.claude/agents/spec.md b/.claude/agents/spec.md new file mode 100644 index 0000000..179e9ce --- /dev/null +++ b/.claude/agents/spec.md @@ -0,0 +1,36 @@ +# Spec Writer Agent + +## Your job +When given a feature idea in plain english, write a structured spec +and save it to specs/{kebab-case-feature-name}.md + +## Spec format (always follow exactly) + +### Problem +What problem does this solve? Who faces it? + +### Solution +What are we building? Simplest possible version. + +### API Contract +Every endpoint needed: +- Method + path +- Request body (with types) +- Response body (with types) +- Error cases + +### Data Model +New DB fields, collections, or tables needed. + +### Edge Cases +What can go wrong? How do we handle each? + +### Out of Scope +What are we explicitly NOT building in this version? + +## Rules +- Max 1 page. Short is better. +- No implementation details — what, not how. +- Always ask: what is the simplest version of this? +- File name: specs/{kebab-case-name}.md +- After writing, print the file path and a 2-line summary. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3304e64 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,61 @@ +name: Deploy to Production + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build with Maven + run: mvn clean package -DskipTests + + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ghcr.io/marava-tech/breakup-backend:latest + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Copy compose files to server + uses: appleboy/scp-action@v1 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + source: "infra/docker-compose.yml,infra/docker-compose.prod.yml" + target: /root/server-setup/services/breakup-stories/ + + - name: Deploy on server + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + script: | + cd /root/server-setup/services/breakup-stories + docker compose -f docker-compose.yml -f docker-compose.prod.yml pull breakup-backend + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps breakup-backend + docker image prune -f diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1ebbf52 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,34 @@ +# Breakup Stories Backend + +## What this is +Breakup Stories app backend. Java 17 + Spring Boot 3. +MongoDB (primary) + MySQL + Redis. Deployed via Docker on Hostinger. +CI/CD via GitHub Actions → ghcr.io → auto-deploy on merge to main. + +## Stack +- Java 17, Spring Boot 3.x +- MongoDB 7 (primary DB) +- MySQL 8 +- Redis 7 (cache only) +- Docker on Hostinger VPS (32GB RAM) + +## Agents — always load before acting +- New feature idea → read .claude/agents/spec.md first +- Writing or fixing code → read .claude/agents/backend.md first +- Testing changes → read .claude/agents/qa.md first +- Git operations → read .claude/agents/git.md first +- Server / infra / Docker → read .claude/agents/devops.md first + +## Workflow (always follow this order) +1. Spec agent writes specs/{feature}.md +2. Backend agent implements from spec +3. QA agent writes and runs tests +4. Git agent: branch → commit → push → raise PR +5. Merge PR → GitHub Actions auto-deploys. Never deploy manually. + +## Hard rules +- Never push directly to main +- Never deploy manually +- Every feature needs a spec file before code is written +- All API routes under /api/v1/ +- Error format: { "error": "message", "code": "ERROR_CODE" } diff --git a/infra/docker-compose.prod.yml b/infra/docker-compose.prod.yml new file mode 100644 index 0000000..52bc567 --- /dev/null +++ b/infra/docker-compose.prod.yml @@ -0,0 +1,4 @@ +services: + breakup-backend: + mem_limit: 4g + cpus: '2.0' diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..dd9a74a --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,42 @@ +services: + breakup-backend: + image: ghcr.io/marava-tech/breakup-backend:latest + container_name: breakup-backend + pull_policy: always + restart: on-failure + deploy: + restart_policy: + condition: on-failure + max_attempts: 3 + ports: + - "9200:8080" + environment: + - SPRING_DATA_MONGODB_HOST=mongodb + - SPRING_DATA_MONGODB_PORT=27017 + - SPRING_DATA_MONGODB_DATABASE=${MONGO_DATABASE:-breakup_stories} + - SPRING_DATA_MONGODB_USERNAME=${MONGO_ROOT_USERNAME} + - SPRING_DATA_MONGODB_PASSWORD=${MONGO_ROOT_PASSWORD} + - SPRING_DATA_MONGODB_AUTHENTICATION_DATABASE=admin + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - BREAKUP_CLOUDINARY_CLOUD_NAME=${BREAKUP_CLOUDINARY_CLOUD_NAME} + - BREAKUP_CLOUDINARY_API_KEY=${BREAKUP_CLOUDINARY_API_KEY} + - BREAKUP_CLOUDINARY_API_SECRET=${BREAKUP_CLOUDINARY_API_SECRET} + - BREAKUP_MAIL_HOST=${BREAKUP_MAIL_HOST} + - BREAKUP_MAIL_PORT=${BREAKUP_MAIL_PORT} + - BREAKUP_MAIL_USERNAME=${BREAKUP_MAIL_USERNAME} + - BREAKUP_MAIL_PASSWORD=${BREAKUP_MAIL_PASSWORD} + networks: + - database-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + +networks: + database-network: + external: true + name: database-network diff --git a/specs/.gitkeep b/specs/.gitkeep new file mode 100644 index 0000000..e69de29 From 834d4a8b704853e8d8a4ad0e0e5313db2b75cc0e Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Tue, 31 Mar 2026 23:16:34 +0530 Subject: [PATCH 02/10] fix: use JDK 21 in deploy workflow --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3304e64..fc7bb80 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,10 +10,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Build with Maven From 04e91b18dec70d001545af30a229c2ed444c6a4c Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Tue, 31 Mar 2026 23:24:48 +0530 Subject: [PATCH 03/10] fix: add packages write permission to workflow --- .github/workflows/deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fc7bb80..3bde582 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,10 @@ on: push: branches: [main] +permissions: + contents: read + packages: write + jobs: build: runs-on: ubuntu-latest From ae192cf18b6e37f777ff4e97d591a61006efe6f7 Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Tue, 31 Mar 2026 23:41:12 +0530 Subject: [PATCH 04/10] fix: use infra/ compose path on server, cache maven deps --- .github/workflows/deploy.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3bde582..3d8ea1b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,6 +20,13 @@ jobs: java-version: '21' distribution: 'temurin' + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-maven- + - name: Build with Maven run: mvn clean package -DskipTests @@ -60,6 +67,6 @@ jobs: key: ${{ secrets.SERVER_SSH_KEY }} script: | cd /root/server-setup/services/breakup-stories - docker compose -f docker-compose.yml -f docker-compose.prod.yml pull breakup-backend - docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps breakup-backend + docker compose -f infra/docker-compose.yml -f infra/docker-compose.prod.yml pull breakup-backend + docker compose -f infra/docker-compose.yml -f infra/docker-compose.prod.yml up -d --no-deps breakup-backend docker image prune -f From 97e581d0cc79cb26e0a2ebdca24db96d6b4215d5 Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Wed, 1 Apr 2026 10:18:11 +0530 Subject: [PATCH 05/10] fix: use server-setup root for env-file, remove old container before redeploy --- .github/workflows/deploy.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3d8ea1b..cd33959 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -66,7 +66,8 @@ jobs: username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SERVER_SSH_KEY }} script: | - cd /root/server-setup/services/breakup-stories - docker compose -f infra/docker-compose.yml -f infra/docker-compose.prod.yml pull breakup-backend - docker compose -f infra/docker-compose.yml -f infra/docker-compose.prod.yml up -d --no-deps breakup-backend + cd /root/server-setup + docker compose -f services/breakup-stories/infra/docker-compose.yml -f services/breakup-stories/infra/docker-compose.prod.yml --env-file .env pull breakup-backend + docker rm -f breakup-backend 2>/dev/null || true + docker compose -f services/breakup-stories/infra/docker-compose.yml -f services/breakup-stories/infra/docker-compose.prod.yml --env-file .env up -d --no-deps breakup-backend docker image prune -f From 3a32010cc8e3b0a0c4799a01069ac2189dd8ef9a Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Wed, 15 Apr 2026 22:42:31 +0530 Subject: [PATCH 06/10] chore: add CLAUDE.md, docs, learnings, and memory context Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 56 +++++++++++++++++++++++++++++---------- docs/architecture.md | 27 +++++++++++++++++++ improvements.md | 31 ---------------------- learnings.md | 62 ++++++++++++++++++++++++++++++++++++++++++++ memory/context.md | 16 ++++++++++++ memory/decisions.md | 16 ++++++++++++ 6 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 docs/architecture.md delete mode 100644 improvements.md create mode 100644 learnings.md create mode 100644 memory/context.md create mode 100644 memory/decisions.md diff --git a/CLAUDE.md b/CLAUDE.md index 1ebbf52..95795d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,29 +1,48 @@ # Breakup Stories Backend ## What this is -Breakup Stories app backend. Java 17 + Spring Boot 3. -MongoDB (primary) + MySQL + Redis. Deployed via Docker on Hostinger. -CI/CD via GitHub Actions → ghcr.io → auto-deploy on merge to main. +Audio story platform API. Java 17 + Spring Boot 3. +MongoDB 7 (primary DB) + Redis 7 (cache only). +CI/CD: GitHub Actions → ghcr.io/marava-tech/breakup-backend:latest → auto-deploy on merge to main. + +## Product context +Users submit breakup/relationship stories. AI (LLM) rewrites/enhances them. TTS converts to audio. Users listen to short-video-style content. OTP auth via email (Gmail SMTP). Stories reviewed by admin before publishing. ## Stack - Java 17, Spring Boot 3.x -- MongoDB 7 (primary DB) -- MySQL 8 +- MongoDB 7 (primary DB — videos, interactions, users, comments) - Redis 7 (cache only) -- Docker on Hostinger VPS (32GB RAM) +- Cloudinary (image and audio uploads, Cloud: dhssmiyoc) +- Gmail SMTP (email OTP) +- LLM integration (story rewriting via StoryRewriteService) +- TTS service (text-to-speech audio generation) +- Transcription service (audio → text) +- Port: 9200 → domain: breakup-backend.marava.tech + +## Project structure +``` +src/main/java/com/breakup/ +├── controller/ ← REST controllers +├── service/ ← business logic (LLM, TTS, transcription, recommendations) +├── repository/ ← Spring Data MongoDB repos +├── model/ ← DB entities (ShortVideo, ShortVideoInteraction, User, etc.) +├── dto/ ← request/response DTOs +├── exception/ ← custom exceptions +└── config/ ← Spring config +``` -## Agents — always load before acting +## Agents — load before acting - New feature idea → read .claude/agents/spec.md first -- Writing or fixing code → read .claude/agents/backend.md first -- Testing changes → read .claude/agents/qa.md first -- Git operations → read .claude/agents/git.md first -- Server / infra / Docker → read .claude/agents/devops.md first +- Writing/fixing code → read .claude/agents/backend.md first +- Testing → read .claude/agents/qa.md first +- Git/PR → read .claude/agents/git.md first +- Server/Docker → read .claude/agents/devops.md first -## Workflow (always follow this order) +## Workflow 1. Spec agent writes specs/{feature}.md 2. Backend agent implements from spec 3. QA agent writes and runs tests -4. Git agent: branch → commit → push → raise PR +4. Git agent: branch → commit → push → PR 5. Merge PR → GitHub Actions auto-deploys. Never deploy manually. ## Hard rules @@ -31,4 +50,13 @@ CI/CD via GitHub Actions → ghcr.io → auto-deploy on merge to main. - Never deploy manually - Every feature needs a spec file before code is written - All API routes under /api/v1/ -- Error format: { "error": "message", "code": "ERROR_CODE" } +- Error format: `{ "error": "message", "code": "ERROR_CODE" }` +- No default values in `@Value` for secrets — fail fast if env var missing +- Add individual `@Indexed` on fields queried alone (not just compound indexes) +- Batch DB calls — never call DB inside a loop (N+1 problem) +- Use atomic MongoDB operations for concurrent mutations (like/unlike) + +## Related docs +- learnings.md — 24 documented bugs fixed + lessons learned +- memory/context.md — current focus +- specs/ — feature specifications diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..57e6d25 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,27 @@ +# Architecture — Breakup Stories Backend + +## System overview + +Flutter app + React admin → Spring Boot REST API → MongoDB (primary) + Redis (cache) + +## Content pipeline +1. User submits story text via Flutter app +2. Admin reviews and approves in dashboard +3. LLM (StoryRewriteService) rewrites/enhances the story +4. TTS (TTSService) converts to audio +5. Audio stored in Cloudinary +6. Video record published to feed +7. Users discover via recommendation feed (ShortVideoRecommendationService) + +## Authentication +- User app: Email OTP via Gmail SMTP → backend JWT +- Admin dashboard: separate admin auth + +## Key services +- `ShortVideoService` — CRUD for video content +- `ShortVideoInteractionService` — likes, comments, shares (atomic operations) +- `ShortVideoRecommendationService` — feed algorithm, language-based filtering +- `StoryRewriteService` — LLM integration for story enhancement +- `TTSService` — text-to-speech audio generation +- `TranscriptionService` — audio → text (with GCS integration) +- `StoryProcessingService` — orchestrates the full content pipeline diff --git a/improvements.md b/improvements.md deleted file mode 100644 index 90b6f17..0000000 --- a/improvements.md +++ /dev/null @@ -1,31 +0,0 @@ -# Improvements — Bug Fixes & Optimizations - -> Each item is fixable in **<100 lines of code**. -> Legend — Priority: 🔴 Critical · 🟠 High · 🟡 Medium · 🟢 Low - -| # | Issue | File / Location | Fix Summary | Status | Priority | -|---|-------|-----------------|-------------|--------|----------| -| 1 | **N+1 query in `getComments()`** — `userService.getUserEntityById()` called inside `.map()` loop, one DB call per comment | `ShortVideoInteractionService.java:86-99` | Batch-fetch all user IDs first with `userService.getUsersByIds(ids)`, then build a lookup map | ✅ Done | 🔴 Critical | -| 2 | **Race condition in like / unlike** — non-atomic check-then-save allows duplicate like records under concurrent requests | `ShortVideoInteractionService.java:40-64` | Replace check + save with MongoDB upsert (`findAndModify` / `@Upsert`) so the operation is atomic | ✅ Done | 🔴 Critical | -| 3 | **Orphan interaction records in `recordShare()`** — interaction saved without verifying the `videoId` exists | `ShortVideoInteractionService.java:142-150` | Add `videoRepository.existsById(videoId)` guard before saving; throw `ResourceNotFoundException` if missing | ✅ Done | 🔴 Critical | -| 4 | **NPE on `user.getGender().toString()`** — user object retrieved by `getUserById()` can be `null` | `StoryProcessingService.java:423` | Wrap with `Optional.ofNullable(user).map(User::getGender).map(Enum::name).orElse("unknown")` | ✅ Done | 🔴 Critical | -| 5 | **Missing `@Valid` on Admin request body** — `ShortVideoRequest` has no validation annotations; null/empty fields accepted | `AdminShortVideoController.java:24,32` · `ShortVideoRequest.java` | Add `@Valid` to controller params + `@NotBlank` / `@NotNull` on required DTO fields | ✅ Done | 🟠 High | -| 6 | **`deleteComment()` silently succeeds on missing ID** — `ifPresent()` swallows the not-found case, no error returned | `ShortVideoInteractionService.java:101-116` | Replace `ifPresent` with `orElseThrow(() -> new ResourceNotFoundException("Comment not found"))` | ✅ Done | 🟠 High | -| 7 | **Missing individual MongoDB indexes on `ShortVideoInteraction`** — only compound index exists; queries on `userId` or `videoId` alone do full scans | `ShortVideoInteraction.java:20-23` | Add `@Indexed` on `videoId` and `userId` fields (compound doesn't cover single-field queries) | ✅ Done | 🟠 High | -| 8 | **Generic `RuntimeException` in `ShortVideoService`** — callers cannot distinguish not-found from other errors | `ShortVideoService.java:44,71` | Throw `ResourceNotFoundException` (already exists in project) instead of raw `RuntimeException` | ✅ Done | 🟠 High | -| 9 | **No max page-size guard** — `size` request param has no upper limit; large values load entire collections into memory | `AdminShortVideoController.java:46` · `ShortVideoController.java` | Clamp `size = Math.min(size, 100)` in service layer or add `@Max(100)` to controller param | ✅ Done | 🟠 High | -| 10 | **Unsafe `choices.get(0)` without bounds check** — throws `IndexOutOfBoundsException` if LLM returns empty choices list | `StoryRewriteService.java:467,625,688` | Guard with `if (choices == null \|\| choices.isEmpty()) throw ...` before `.get(0)` | ✅ Already Fixed | 🟠 High | -| 11 | **`HttpURLConnection` never closed** — no `disconnect()` call; connections accumulate and can exhaust the pool | `TranscriptionService.java:118` | Add `connection.disconnect()` in `finally` block (or switch to try-with-resources via wrapper) | ✅ Done | 🟠 High | -| 12 | **Hardcoded Cloudinary credentials as `@Value` defaults** — if env vars are missing the real secret is used as fallback in plaintext | `CloudinaryConfig.java:18,21,24` | Remove default values from `@Value`; add `@PostConstruct` check that throws on null/empty | ✅ Done | 🟠 High | -| 13 | **Language stored as free-form `String` in `ShortVideo`** — allows invalid codes like `"xyz"` or `"en-US"` | `ShortVideo.java:37` | Replace with `enum VideoLanguage { te, ta, hi, kn, ml, en }` and update repository / service accordingly | ✅ Done | 🟡 Medium | -| 14 | **No logging for auth failures** — unauthorized access in `requireUserId()` is thrown silently, no audit trail | `ShortVideoController.java:110-116` | Add `log.warn("Unauthorized access attempt - no userId in auth token")` before throwing | ✅ Done | 🟡 Medium | -| 15 | **`audioChunks` list not cleared on TTS exception** — memory held for all processed chunks if a later chunk fails | `TTSService.java:155-169` | Add `audioChunks.clear()` in the `catch` block before rethrowing | ✅ Done | 🟡 Medium | -| 16 | **Confidence threshold too low (0.2)** — accepts very poor transcriptions without surfacing them | `TranscriptionService.java:483` | Raise threshold from `0.2` → `0.5`; optionally make it a configurable property | ✅ Done | 🟡 Medium | -| 17 | **N+1 full-document load in `getFeed()`** — fetches 2 000 full `ShortVideoInteraction` docs just to extract `videoId` strings | `ShortVideoRecommendationService.java:32-42` | Add `findVideoIdsByUserIdAndType()` with `@Query` + `@Field` projection to return only `videoId` | ✅ Done | 🟡 Medium | -| 18 | **Hardcoded `total = 10000L` in `PagedResponse`** — fake total breaks accurate pagination on the client | `ShortVideoRecommendationService.java:66` | Use `videoRepository.countByLanguageIn(languages)` for real total, or return `-1` as "unknown" sentinel | ✅ Done | 🟡 Medium | -| 19 | **Inconsistent `@PreAuthorize` pattern** — `DefaultConfigController` uses `hasAuthority('ROLE_ADMIN')` while others use `hasRole('ADMIN')` | `DefaultConfigController.java:40,47,55,63,152` | Standardise to `@PreAuthorize("hasRole('ADMIN')")` across all admin controllers | ✅ Done | 🟡 Medium | -| 20 | **GCS cleanup silently swallows exceptions** — `cleanupGcs()` catch logs at `WARN` but object URI never included; leak undetectable | `TranscriptionService.java:301-306` | Log at `ERROR` level and include the full GCS URI (`gs://bucket/object`) in the message | ✅ Done | 🟡 Medium | -| 21 | **`delete()` return value ignored on temp file** — deletion can fail silently, accumulating temp files on disk | `TranscriptionService.java:93-97` | `if (!audioFile.delete()) log.warn("Failed to delete temp file: {}", audioFile.getPath())` | ✅ Done | 🟢 Low | -| 22 | **Missing index on `ShortVideoComment.parentId`** — nested-comment queries (replies) do full collection scans | `ShortVideoComment.java` | Add `@Indexed` to the `parentId` field | ✅ Done | 🟢 Low | -| 23 | **Regex recompiled per iteration in emotional-word loop** — `replaceAll("(?i)" + word, ...)` inside a loop recompiles the pattern each time | `TTSService.java:279-284` | Pre-build a `Map` of compiled patterns once; reuse in loop | ✅ Done | 🟢 Low | -| 24 | **O(n) MP3 header scan over full audio chunk** — byte-by-byte search through potentially megabytes of audio data | `TTSService.java:210-219` | Limit scan to first 256 bytes (MP3 frames start at the beginning); skip rest of array | ✅ Done | 🟢 Low | diff --git a/learnings.md b/learnings.md new file mode 100644 index 0000000..dde6394 --- /dev/null +++ b/learnings.md @@ -0,0 +1,62 @@ +# Learnings — Breakup Stories Backend + +> All lessons extracted from the improvements.md bug fixes. + +--- + +## Database & MongoDB + +- **N+1 query in loops**: Calling `userService.getUserEntityById()` inside a `.map()` loop causes one DB call per item. Fix: batch-fetch all IDs first with `getUsersByIds(ids)`, build a lookup map, then map without DB calls. (`ShortVideoInteractionService.java`) +- **N+1 in feed queries**: `getFeed()` fetched 2000 full ShortVideoInteraction documents just to extract videoId strings. Fix: add a projection query `findVideoIdsByUserIdAndType()` with `@Field` to return only `videoId`. +- **Compound indexes don't cover single-field queries**: Only compound index on ShortVideoInteraction existed. Queries on `userId` alone or `videoId` alone did full collection scans. Add `@Indexed` on individual fields even when compound index exists. +- **Missing index on `parentId`**: Nested-comment (replies) queries did full scans on ShortVideoComment. Add `@Indexed` to `parentId` field. +- **Hardcoded total in pagination**: `total = 10000L` was hardcoded in PagedResponse. Use real count query or `-1` sentinel for "unknown total". + +## Concurrency & Atomicity + +- **Race condition in like/unlike**: Non-atomic check-then-save allows duplicate like records under concurrent requests. Fix: replace check + save with MongoDB upsert (`findAndModify` with `@Upsert`). (`ShortVideoInteractionService.java`) + +## Null Safety + +- **NPE on `.getGender().toString()`**: User object from `getUserById()` can be null. Always wrap: `Optional.ofNullable(user).map(User::getGender).map(Enum::name).orElse("unknown")`. (`StoryProcessingService.java`) +- **Unsafe `choices.get(0)` without bounds check**: Throws `IndexOutOfBoundsException` if LLM returns empty choices. Always guard: `if (choices == null || choices.isEmpty()) throw ...` (`StoryRewriteService.java`) + +## Validation + +- **Missing `@Valid` on request bodies**: `ShortVideoRequest` had no validation — null/empty fields were accepted. Add `@Valid` to controller params + `@NotBlank`/`@NotNull` on required DTO fields. +- **Orphan records without entity check**: `recordShare()` saved interaction without verifying the `videoId` exists. Add `videoRepository.existsById(videoId)` guard before saving. +- **No max page-size guard**: `size` request param with no upper limit loads entire collections. Clamp: `Math.min(size, 100)` in service layer or `@Max(100)` on controller param. + +## Error Handling + +- **Generic `RuntimeException` loses context**: Callers can't distinguish not-found from other errors. Throw `ResourceNotFoundException` (already exists) instead of raw `RuntimeException`. +- **`deleteComment()` swallows not-found**: `ifPresent()` silently succeeds on missing ID. Replace with `orElseThrow(() -> new ResourceNotFoundException("Comment not found"))`. +- **GCS cleanup swallows exceptions**: `cleanupGcs()` logged at WARN without the GCS URI. Log at ERROR and include full URI (`gs://bucket/object`). (`TranscriptionService.java`) +- **Temp file delete failure ignored**: `audioFile.delete()` return value was ignored. Log: `if (!audioFile.delete()) log.warn("Failed to delete temp file: {}", audioFile.getPath())`. + +## Resource Management + +- **`HttpURLConnection` never closed**: No `disconnect()` call — connections accumulate. Add `connection.disconnect()` in `finally` block. (`TranscriptionService.java`) +- **`audioChunks` list not cleared on TTS exception**: Memory held for all processed chunks if a later chunk fails. Add `audioChunks.clear()` in `catch` block before rethrowing. + +## Configuration + +- **Hardcoded Cloudinary credentials as `@Value` defaults**: `@Value("${prop:realSecretHere}")` leaks secrets in plaintext if env var missing. Remove all defaults from `@Value` for secrets. Add `@PostConstruct` check that throws on null/empty. (`CloudinaryConfig.java`) + +## Performance + +- **Regex recompiled per iteration**: `replaceAll("(?i)" + word, ...)` inside a loop recompiles the pattern each time. Pre-build a `Map` of compiled patterns once; reuse in loop. (`TTSService.java`) +- **O(n) MP3 header scan over full audio**: Byte-by-byte search through megabytes of audio data. Limit scan to first 256 bytes — MP3 frames always start at the beginning. + +## Type Safety + +- **Language stored as free-form String**: Allowed invalid codes like `"xyz"`. Replace with `enum VideoLanguage { te, ta, hi, kn, ml, en }` and update repositories. + +## Logging & Observability + +- **No logging for auth failures**: Unauthorized access in `requireUserId()` threw silently with no audit trail. Add `log.warn("Unauthorized access attempt - no userId in auth token")` before throwing. +- **Confidence threshold too low (0.2)**: Accepted very poor transcriptions. Raise to `0.5`; make it a configurable property. + +## Auth Patterns + +- **Inconsistent `@PreAuthorize`**: `DefaultConfigController` used `hasAuthority('ROLE_ADMIN')` while others used `hasRole('ADMIN')`. Standardize to `@PreAuthorize("hasRole('ADMIN')")` across all admin controllers. diff --git a/memory/context.md b/memory/context.md new file mode 100644 index 0000000..9fff208 --- /dev/null +++ b/memory/context.md @@ -0,0 +1,16 @@ +# Breakup Stories Backend — Current Context + +Last updated: 2026-04-01 + +## Active focus +Production stable. All 24 bugs from improvements.md fixed. + +## Recently completed +- Fixed all 24 issues documented in improvements.md (N+1 queries, race conditions, null safety, resource leaks, validation, auth consistency) + +## Next up +- Check for any remaining unused endpoints or dead code +- Review transcription confidence threshold configuration + +## Notes +Update this file at the start of each working session. diff --git a/memory/decisions.md b/memory/decisions.md new file mode 100644 index 0000000..befb499 --- /dev/null +++ b/memory/decisions.md @@ -0,0 +1,16 @@ +# Architecture Decisions — Breakup Stories Backend + +## 2026-04-01 — MongoDB as primary DB +**Decision:** MongoDB for all video/interaction/user data. +**Why:** Document model fits the flexible short-video content structure with varying metadata per language/genre. +**Outcome:** Working well. Required careful index management (see learnings.md). + +## 2026-04-01 — Atomic operations for like/unlike +**Decision:** Use MongoDB upsert (`findAndModify` with `@Upsert`) for like/unlike operations. +**Why:** Race condition with non-atomic check-then-save caused duplicate like records under concurrent requests. +**Outcome:** Fixed the race condition. Always use atomic operations for concurrent mutations. + +## 2026-04-01 — Cloudinary for media storage +**Decision:** Cloudinary handles all image and audio file storage. +**Why:** Built-in transformations, CDN delivery, and simple SDK. Avoid managing MinIO for media files. +**Cloud name:** dhssmiyoc From 0be028fa4772afcdcb351f4d5a0ede33b0dfc39b Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Sat, 20 Jun 2026 14:15:30 +0530 Subject: [PATCH 07/10] security: remove hardcoded credential defaults from application.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All secrets now require explicit env vars — no defaults that could leak real credentials if the env var is unset. Co-Authored-By: Claude Sonnet 4.6 --- src/main/resources/application.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9999061..fe033d0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -41,7 +41,7 @@ spring: host: ${BREAKUP_MAIL_HOST:smtp.gmail.com} port: ${BREAKUP_MAIL_PORT:587} username: ${BREAKUP_MAIL_USERNAME:marava.technologies@gmail.com} - password: ${BREAKUP_MAIL_PASSWORD:REDACTED_GMAIL_PASSWORD} + password: ${BREAKUP_MAIL_PASSWORD} properties: mail: smtp: @@ -66,18 +66,14 @@ server: # JWT Configuration jwt: - secret: ${JWT_SECRET:REDACTED_JWT_SECRET} + secret: ${JWT_SECRET} expiration: 2592000000 # 30 days in milliseconds # Admin Configuration admin: email: ${ADMIN_EMAIL:kinneramadhu123@gmail.com} - # Default secret if not provided in env. Consider rotating this or forcing env var in prod. - # This corresponds to "JBSWY3DPEHPK3PXP" or similar base32 secret. - # Using the generated one: 59cc4505b1378606 (hex) -> need to convert or just put one here. - # User prompt example: JBSWY3DPEHPK3PXP - totp-secret: ${ADMIN_TOTP_SECRET:REDACTED_TOTP_SECRET} + totp-secret: ${ADMIN_TOTP_SECRET} logging: @@ -128,9 +124,9 @@ management: # Cloudinary Configuration for Breakup Stories app: cloudinary: - cloud-name: ${BREAKUP_CLOUDINARY_CLOUD_NAME:dhssmiyoc} - api-key: ${BREAKUP_CLOUDINARY_API_KEY:REDACTED_CLOUDINARY_KEY} - api-secret: ${BREAKUP_CLOUDINARY_API_SECRET:REDACTED_CLOUDINARY_SECRET} + cloud-name: ${BREAKUP_CLOUDINARY_CLOUD_NAME} + api-key: ${BREAKUP_CLOUDINARY_API_KEY} + api-secret: ${BREAKUP_CLOUDINARY_API_SECRET} story: view-dedup-ttl-seconds: ${VIEW_DEDUP_TTL_SECONDS:1800} # Default 30 minutes @@ -153,7 +149,7 @@ google: # OpenAI Configuration for AI Processing openai: api: - key: ${OPENAI_API_KEY:REDACTED_OPENAI_KEY} + key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL:https://api.openai.com/v1} model: ${OPENAI_MODEL:gpt-4o} max-tokens: ${OPENAI_MAX_TOKENS:2048} From f22b3803e0b615f64d4afc6868ba9b04827845a2 Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Sat, 27 Jun 2026 02:49:14 +0530 Subject: [PATCH 08/10] fix(ci): update deploy to use server-v2 compose, drop SCP of repo compose files --- .github/workflows/deploy.yml | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cd33959..f12404d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -50,15 +50,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Copy compose files to server - uses: appleboy/scp-action@v1 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - key: ${{ secrets.SERVER_SSH_KEY }} - source: "infra/docker-compose.yml,infra/docker-compose.prod.yml" - target: /root/server-setup/services/breakup-stories/ - - name: Deploy on server uses: appleboy/ssh-action@v1 with: @@ -66,8 +57,6 @@ jobs: username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SERVER_SSH_KEY }} script: | - cd /root/server-setup - docker compose -f services/breakup-stories/infra/docker-compose.yml -f services/breakup-stories/infra/docker-compose.prod.yml --env-file .env pull breakup-backend - docker rm -f breakup-backend 2>/dev/null || true - docker compose -f services/breakup-stories/infra/docker-compose.yml -f services/breakup-stories/infra/docker-compose.prod.yml --env-file .env up -d --no-deps breakup-backend + docker compose -f /root/server-v2/services/breakup-backend/docker-compose.yml --env-file /root/server-v2/.env pull breakup-backend + docker compose -f /root/server-v2/services/breakup-backend/docker-compose.yml --env-file /root/server-v2/.env up -d --no-deps breakup-backend docker image prune -f From 1f7297c1e730a1586406fb3656cf1788fb9b2876 Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Sat, 27 Jun 2026 02:50:14 +0530 Subject: [PATCH 09/10] fix(ci): trigger on active branch, use server-v2 compose path --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f12404d..c41908d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy to Production on: push: - branches: [main] + branches: [main, feat/add-claude-instructions] permissions: contents: read From 8a5e7c5b0e34a344b20d0656d4c42b4fd2a62c91 Mon Sep 17 00:00:00 2001 From: Madhu Kinnera Date: Sat, 27 Jun 2026 13:46:05 +0530 Subject: [PATCH 10/10] =?UTF-8?q?fix(ci):=20resolve=20merge=20conflict,=20?= =?UTF-8?q?update=20paths=20server-v2=20=E2=86=92=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .claude/agents/devops.md | 2 +- .github/workflows/deploy.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/agents/devops.md b/.claude/agents/devops.md index 50212c3..bdf79e0 100644 --- a/.claude/agents/devops.md +++ b/.claude/agents/devops.md @@ -9,7 +9,7 @@ for Abhedyam and Breakup Stories on the shared Hostinger server. - User: root - Abhedyam path: /opt/abhedyam/ - Breakup path: /opt/breakup/ -- Server setup: /root/server-setup/ +- Server setup: /root/server/ ## All running containers | Container | Image | Port | diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c41908d..e77ce19 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -57,6 +57,6 @@ jobs: username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SERVER_SSH_KEY }} script: | - docker compose -f /root/server-v2/services/breakup-backend/docker-compose.yml --env-file /root/server-v2/.env pull breakup-backend - docker compose -f /root/server-v2/services/breakup-backend/docker-compose.yml --env-file /root/server-v2/.env up -d --no-deps breakup-backend + docker compose -f /root/server/services/breakup-backend/docker-compose.yml --env-file /root/server/.env pull breakup-backend + docker compose -f /root/server/services/breakup-backend/docker-compose.yml --env-file /root/server/.env up -d --no-deps breakup-backend docker image prune -f