Module 8 · Capstone · Drills
Integration is where projects break. These drills are the breakages you will hit wiring DocChat together — CORS, 401s, empty retrieval, env mismatch — and the exact way to trace and fix each one. Do them at the keyboard, with your real app.
Drill 1 CORS
You log in from the Next.js UI and the console shows: Access to fetch ... blocked by CORS policy. The API works fine in /docs. Fix it.
The API runs on a different origin than the frontend, so the browser blocks it. The API must allow the frontend's origin. In app/main.py:
from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000"], # the frontend origin allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
CORS is enforced by the browser, not the server — that's why /docs and curl work but the React app doesn't. The fix is always on the backend, on the Next.js↔FastAPI boundary.
Drill 2 JWT
Login succeeds and returns a token, but every later request to /documents returns 401 Unauthorized. Attach the token correctly.
The token has to ride on every protected call as a Bearer header. Centralise it in one fetch wrapper so you can't forget:
// lib/api.ts export async function apiFetch(path, options = {}) { const token = localStorage.getItem("token"); return fetch(`${process.env.NEXT_PUBLIC_API_URL}${path}`, { ...options, headers: { "Content-Type": "application/json", ...options.headers, Authorization: `Bearer ${token}`, # the missing piece }, }); }
A 401 means "I don't know who you are" — the header is missing or malformed. A 403 would mean "I know you, but you can't have this". Different boundary, different fix.
Drill 3 trace the ask
A user asks a question and gets back an empty or nonsense answer — no error, just bad output. Trace the /ask pipeline to find where it breaks.
Walk the pipeline in order and print at each stage — the break is wherever the data stops looking right:
1. Question received? → log the incoming question string 2. Question embedded? → log the vector length (should be non-zero) 3. Chunks retrieved? → log how many rows vector search returned 4. Prompt built? → log the final prompt (chunks present?) 5. LLM called? → log the raw model response
Most common find: step 3 returns zero chunks — meaning ingestion never populated chunks for this document, or the query is scoped to the wrong user_id/document_id. If chunks come back but the answer is empty, the break is step 4: the chunks weren't injected into the prompt.
Binary-search the pipeline: check the middle (retrieval) first. If chunks are there, the bug is downstream (prompt/LLM); if not, it's upstream (ingestion/scoping).
Drill 4 env vars
Locally everything works; deployed, the frontend can't reach the API. Verify the env vars across both apps to find the mismatch.
The two boundary values must agree. Print/inspect them on each side:
# backend (.env): the API must allow the deployed frontend origin FRONTEND_URL=https://docchat-web.vercel.app # frontend (.env): must point at the deployed API, not localhost NEXT_PUBLIC_API_URL=https://docchat-api.onrender.com
Classic mistake: NEXT_PUBLIC_API_URL still says http://localhost:8000 in production, or FRONTEND_URL still says localhost:3000 so CORS rejects the real site. Both must be the deployed URLs, and they must point at each other.
Remember: NEXT_PUBLIC_ vars are baked in at build time — change one and you must rebuild/redeploy the frontend for it to take effect.
Tick each only when it genuinely works in your app — not when you "wrote the code".
users, documents, chunks migrated/docs opens with no errorsdocuments rowchunks/ask response includes the source chunksuser_id from the JWTlib/api.ts attaches the JWT to every protected requestChallenge · troubleshoot the common breakages
It won't work first try. Use this map to locate the failure by its symptom, then fix the boundary it points to.
| Symptom | Boundary | Fix |
|---|---|---|
| "Blocked by CORS policy" in console | Next.js ↔ FastAPI | Add the frontend origin to allow_origins (Drill 1) |
| 401 on every protected call | frontend token handling | Attach Authorization: Bearer in api.ts (Drill 2) |
| Answer empty or irrelevant | RAG pipeline | Trace retrieval — usually zero chunks or wrong scoping (Drill 3) |
| Works local, fails deployed | env vars | Point both URL vars at the deployed apps; rebuild frontend (Drill 4) |
| Upload succeeds, ask finds nothing | ingestion write path | Confirm chunks rows + embeddings were actually written |
The discipline: read the error → name the boundary → check that boundary's config. Almost every integration bug is one of these five.
Click a card to flip it. Say the answer out loud before you flip — that's the rep that locks the architecture in.
chunks.embedding column in Postgres (via pgvector) — not in the model.