Codebase atlas
Three websites ship from a single static export, and one FastAPI container serves everything dynamic across all of them. This is the map an engineer would want before opening the repository: what each layer owns, the two pipelines that explain the folder structure, and the handful of conventions where getting it wrong produces no error at all.
Orientation
This started as a portfolio and grew two products. All three ship from a single Next.js static export; one FastAPI container serves everything dynamic across all of them.
jayaremala.com
Career pages, blog, lab logs, gallery — plus Avocado, a RAG chatbot at /chat answering from the knowledge base.
gradevitian.jayaremala.com
Student tools for VIT: GPA/CGPA calculators, attendance and grade predictors, a semester planner, accounts and badges, and Q&A over the academic regulations.
vrfbricks.jayaremala.com
A working fly-ash brick yard in Kavali. Bilingual English/Telugu, WhatsApp-first conversion, and a brick-quantity calculator the old site never had.
GitHub Pages ──serves──▶ jayaremala.com ┐
│ same out/ folder,
nginx @ Lightsail ─┬─▶ gradevitian.jayaremala.com │ built once by CI
└─▶ vrfbricks.jayaremala.com ┘
│
└─▶ api.jayaremala.com ──▶ Docker :8000 (FastAPI)
│
/data volume ├── chroma_db/ (vectors)
├── analytics.db
├── content.db
└── gradevitian.dbThere is no Node server in production. Next.js runs with output: "export", so every page is pre-rendered HTML. Anything live — chat, analytics, logins, saved calculators — is a client-side fetch to the API box.
Mechanism
Almost every “why is this file here?” question resolves into one of these. Learn them and the folder structure stops being surprising.
One source of truth
frontend/src/data/knowledge/ is overwritten on every build, and so are blog.json and lab.json. The editable originals are the backend JSON and the MDX files. The sync runs automatically from the predev/prebuild hooks, so it is never a step you have to remember.
Service
One FastAPI app behind api.jayaremala.com. It serves the chatbot, every surface's analytics, the gradeVITian product API, the content CMS, and a public MCP server — from a single container.
| Package | Responsibility |
|---|---|
| rag/ | The retrieval engine. store.py runs ChromaDB with fastembed ONNX embeddings (no PyTorch), a BM25 index, and Reciprocal Rank Fusion over both. ingest.py builds the corpus and is hash-gated, so it only rebuilds when the knowledge JSON actually changed. graph.py pulls in adjacent skill and role documents, so an answer about a project also knows the stack it was built on. |
| routers/ai.py | The heart of Avocado, and the largest module. Streaming chat over SSE, an agentic tool-calling mode, HyDE query expansion, prompt assembly, and the provider fallback loop that walks Gemini → Groq → OpenRouter when a free tier returns 429. |
| routers/gradevitian.py | The whole student product: signup and login, password reset, saved calculations, per-calculator persisted state, badges, referrals, moderated comments, and rulebook Q&A. |
| agent/tools.py | One read-only tool registry, shared by Agent mode and the public MCP server — so both expose exactly the same surface, and mcp_server.py is only 49 lines. |
| db/ | Three SQLite stores on a persistent volume: analytics, admin-authored content, and gradeVITian accounts. Visitor IPs are SHA-256 hashed and never stored raw. |
| core/ | Settings, rate limiting, and gradeVITian auth — the last written against the standard library only, with no PyJWT or bcrypt dependency. Also a dependency-free comment moderator that normalises obfuscation before escalating anything borderline to a model. |
| integrations/ | Google Calendar (real free/busy, so booking answers are honest), Gmail, Drive résumé sync, and the weekly digest. All share one OAuth token lifecycle. |
| obs/trace.py | Forty lines of per-request stage timing. Everything the /system dashboard draws comes from here. |
Client
One app, three products. The organising principle is the feature vertical: everything belonging to one product lives together, and only genuinely content-free code sits in a shared bucket.
frontend/src/ ├── app/ routes — (portfolio) group, chat, admin, │ gradevitian, vrfbricks ├── components/ │ ├── ui/ generic primitives — no product content │ ├── portfolio/ chat/ blog/ lab/ system/ admin/ │ └── gradevitian/ vrfbricks/ ├── lib/ api/ content/ portfolio/ admin/ + per-site ├── data/ knowledge/ (generated) + typed *.ts re-exports └── content/ blog/*.mdx lab/*.mdx
The placement rule
If a component renders product-specific content it goes in that product's folder. It belongs in components/ui/ only if it is content-free and at least two verticals could use it.
| Area | What lives there |
|---|---|
| app/(portfolio)/ | The route group. Parentheses mean it adds no URL segment, so (portfolio)/page.tsx is / — there is no /portfolio route. Everything inside shares one nav and footer. |
| app/globals.css | The entire design system, including the Tailwind 4 configuration under @theme inline. There is no tailwind.config.js — v4 does not read one. |
| components/chat/ | Avocado's client. ChatInterface orchestrates SSE consumption, session persistence, agent-mode switching, and the model badge that updates when a fallback fires. AnswerTrace and AgentSteps are the transparency layer: which chunks were retrieved, which tools ran. |
| components/gradevitian/ | Forty-plus components, and none of them hold arithmetic — every formula is a pure function in lib/gradevitian/calc.ts, ported from the original site's client-side JS so it stays testable and shared. |
| components/vrfbricks/ | The brick yard. The enquiry form is a WhatsApp message composer rather than a form submission, because WhatsApp is how that market actually buys. BrickDiagramis generated from the brick's real measurements — a derived drawing, not an illustration. |
| lib/*/use*Base.ts | Both subdomains are served at two mount points from the same build. These hooks return "" on the subdomain and the segment prefix on the main domain, and GVLink/VRFLink apply it. Fourteen lines each, and entirely load-bearing. |
| lib/portfolio/site-nav.tsx | One list of pages that the nav, the footer, the sitemap and the command palette all derive from — so a new page appears in every one of them at once. |
Reference
The decisions worth knowing before a first change — chosen because getting them wrong produces no error, just wrong output.
| Convention | What happens otherwise |
|---|---|
| Edit the backend JSON | Changes to the frontend's copy are wiped by the next build, with no warning. |
| Run from backend/ | Starting from backend/src/ creates a second, empty vector store at a different relative path. Nothing errors; the chatbot simply knows nothing. |
| Keep the paths-filter current | It decides what deploys. A missing entry means the change ships nothing and CI still reports success. |
| Link via GVLink / VRFLink | A raw next/link works on whichever mount point you tested and breaks on the other one. |
| Sort posts by publishedAt | date is an editable display string. Fixing a typo in it would silently reorder the index; publishedAt is set once and never touched. |
| No new Date() in client code | On a static export the HTML is rendered once at build but the component reruns in every browser — so a year-rollover produces a hydration mismatch for every visitor. The build year is inlined as an environment literal instead. |
| Pin fastmcp deliberately | A patch release added Host allow-list enforcement; an implicit bump between local and CI once took the deployed MCP server down. It is now pinned to a narrow range on purpose. |
| Ship a 404/ route per subdomain | A static export never emits a segment's not-found.tsx as a real file, so each site needs a literal route for nginx to point error_page at. |
| Never invent VRF Bricks facts | It is a real business. Unverified details live in a NEEDS_CONFIRMATION block where rendering skips them, rather than being guessed into the page. |
The full source is on GitHub, and the lab entry covers why each layer was built the way it was.