Module 8 · Capstone · Deep Dive

Capstone Build · DocChat

This is the week it all becomes one thing. Seven modules of separate skills — Python, FastAPI, Postgres, auth, React, Next.js, RAG, deploy — now snap together into a single deployed app you can demo and talk through in an interview.

ArchitectureAssemblyShip

Why this matters Every previous lesson built a piece. An interviewer doesn't hire pieces — they hire someone who can show a whole system and explain how the parts talk to each other. DocChat is that system: sign up, upload a PDF, ask a question, get an answer grounded in your own document. This lesson is the integration map — how the pieces wire together, in what order, and where they connect.
In this lesson
  1. The architecture
  2. The data model recap
  3. The end-to-end user flow
  4. Repo & folder structure
  5. Build milestones & order of assembly
  6. How the pieces connect
  7. Build: assemble the happy path
  8. Check yourself

1 · The architecture

DocChat is a classic decoupled full-stack app: a Next.js frontend, a FastAPI backend, a Postgres database with vectors, and an LLM for generation. Each box owns one job and talks to the next over a clear boundary.

  ┌──────────┐   HTTPS    ┌────────────┐   HTTP+JWT   ┌──────────┐
  │ Browser  │ ─────────▶ │  Next.js   │ ───────────▶ │ FastAPI  │
  │  (user)  │            │ (frontend) │              │  (API)   │
  └──────────┘ ◀───────── └────────────┘ ◀─────────── └────┬─────┘
                  HTML/JSON                JSON              │
                                                            │ SQL + vector search
                                                            ▼
                                                  ┌───────────────────┐
                                                  │ Postgres+pgvector  │
                                                  │ (data + embeddings)│
                                                  └─────────┬─────────┘
                                                            │ retrieved chunks
                                                            ▼
                                                  ┌───────────────────┐
                                                  │   LLM (Claude /    │
                                                  │  OpenAI) → answer  │
                                                  └───────────────────┘

Read each arrow as a contract:

ArrowWhat crosses it
Browser → Next.jsThe user's clicks and form submits. Next.js serves the UI (server + client components) and runs in the browser.
Next.js → FastAPIfetch calls carrying JSON and a JWT in the Authorization header. This is the network boundary where CORS lives.
FastAPI → PostgresSQLAlchemy sessions: row reads/writes and vector similarity search (ORDER BY embedding <=> query) via pgvector.
FastAPI → LLMA prompt built from the user's question plus the retrieved chunks. The model returns the grounded answer text.
PHP bridge: think of FastAPI as your old PHP API layer and Next.js as a much smarter front-end than jQuery sprinkled on a page. The big shift: the frontend is now its own deployed app, not templates rendered by the backend.

2 · The data model recap

Three tables carry the whole app. You built these across Modules 3 and 6 — here's how they relate:

users           documents              chunks
─────           ─────────              ──────
id  (PK)        id        (PK)         id          (PK)
email           user_id   (FK→users)  document_id (FK→documents)
password_hash   filename               content      (the text slice)
created_at      status                 embedding    (vector — pgvector)
                created_at             chunk_index

One user owns many documents; one document is split into many chunks; each chunk stores both its text and its embedding (the vector). Retrieval searches the embedding column; generation reads the matching content.

The one rule that keeps it secure Every query for documents and chunks is scoped by user_id, taken from the JWT — never from the request body. A user must only ever retrieve over their own documents. Forgetting this is the most common security bug in this kind of app.

3 · The end-to-end user flow

Trace one full journey. Every step maps to a module you've already built — this is the "story" you'll tell an interviewer.

StepWhat happensBuilt in
1. Sign up / log inFrontend posts credentials → FastAPI hashes/verifies → returns a JWT. Frontend stores it and sends it on every later call.3.3 Auth
2. Upload a PDFUser picks a file → frontend POSTs it to /documents with the JWT → a documents row is created with status processing.2.2 FastAPI
3. Ingest → chunk → embedBackend extracts the text, splits it into chunks, embeds each chunk, and writes chunks rows with their vectors. Status flips to ready.6.2 Building RAG
4. Ask a questionUser types a question → frontend POSTs it to /ask with the JWT and a document id.5.2 Next.js
5. Retrieve → generateBackend embeds the question, runs vector search for the closest chunks, injects them into a prompt, and calls the LLM.6.2 Building RAG
6. Display answer + sourcesFrontend renders the answer and lists the source chunks it was grounded in — so the user can trust it.4.2 Hooks
Interview gold: being able to narrate these six steps cold — naming the JWT, the chunking, the vector search, the context injection — is exactly what separates a hire from a "knows some React".

4 · Repo & folder structure

Two repos (or one monorepo with two folders). Keep them clean — recruiters open these. A practical, 2026-standard layout:

backend FastAPI repo

docchat-api/
docchat-api/
├── app/
│   ├── main.py            # FastAPI app, CORS, router includes
│   ├── config.py          # settings from env (Pydantic Settings)
│   ├── database.py        # engine + session dependency
│   ├── models.py          # User, Document, Chunk (SQLAlchemy)
│   ├── schemas.py         # Pydantic request/response models
│   ├── auth.py            # hashing, JWT create/verify, get_current_user
│   ├── routers/
│   │   ├── auth.py        # /register, /login
│   │   ├── documents.py   # /documents (upload, list)
│   │   └── chat.py        # /ask
│   └── rag/
│       ├── ingest.py      # extract → chunk → embed → store
│       └── retrieve.py    # embed question → vector search → prompt
├── alembic/               # migrations
├── .env                   # DATABASE_URL, JWT_SECRET, LLM_API_KEY (gitignored)
├── Dockerfile
└── requirements.txt

frontend Next.js repo

docchat-web/
docchat-web/
├── app/
│   ├── layout.tsx         # root layout
│   ├── page.tsx           # landing
│   ├── login/page.tsx     # auth form
│   └── chat/page.tsx      # upload + ask UI
├── components/
│   ├── UploadBox.tsx
│   ├── ChatWindow.tsx
│   └── SourceList.tsx
├── lib/
│   └── api.ts             # fetch wrapper that attaches the JWT
├── .env.local            # NEXT_PUBLIC_API_URL (gitignored)
└── package.json

The single most important file for integration is lib/api.ts — one place that knows the backend URL and attaches the token to every request. Centralise it and the whole frontend wires up cleanly.

5 · Build milestones & order of assembly

You don't wire everything at once. Assemble in the order data flows, proving each seam before adding the next. Wire the backend spine first (it's the contract the frontend depends on), then the frontend, then the AI.

#MilestoneProves the seam
1DB + models migrated; FastAPI bootsPostgres ↔ SQLAlchemy
2Register / login returns a JWTAuth works end to end
3Protected /documents upload + listJWT actually guards routes
4Ingestion pipeline fills chunksRAG write path
5/ask retrieves + generates an answerRAG read path + LLM
6Next.js login page hits the APICORS + token storage
7Upload + chat UI wired to the APIFull happy path
8Deploy both; set production env varsIt's live → 7.2 Deploy & CI
Resist the urge to build UI first A pretty chat box with no working backend tells you nothing. Test the API with the Swagger docs (/docs) at every milestone — when the backend is solid, the frontend is just wiring. Backend spine before frontend skin.

6 · How the pieces connect

Three integration concerns connect the boxes. Get these three right and the app holds together.

CORS letting the browser talk to the API

The frontend and backend live on different origins (different URL/port). Browsers block cross-origin requests unless the API explicitly allows them. In FastAPI:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=[settings.FRONTEND_URL],  # e.g. http://localhost:3000
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Forget this and the browser console shows the classic "blocked by CORS policy" error — even though the API itself is fine.

JWT the token that travels on every call

Login returns a token. The frontend stores it and attaches it as Authorization: Bearer <token> on every protected request. The backend's get_current_user dependency decodes it, finds the user, and scopes all queries to them.

// lib/api.ts — attach the token once, everywhere
export async function apiFetch(path, options = {}) {
  const token = localStorage.getItem("token");
  return fetch(`${process.env.NEXT_PUBLIC_API_URL}${path}`, {
    ...options,
    headers: { ...options.headers, Authorization: `Bearer ${token}` },
  });
}

Env vars no secrets, no hardcoded URLs

The two apps share zero code but must agree on the boundary. Each reads its config from env:

AppVarPurpose
backendDATABASE_URLPostgres connection string
backendJWT_SECRETsigns/verifies tokens
backendLLM_API_KEYcalls the model
backendFRONTEND_URLthe CORS allow-list origin
frontendNEXT_PUBLIC_API_URLwhere the API lives

The two values that must match across apps: the frontend's NEXT_PUBLIC_API_URL points at the backend, and the backend's FRONTEND_URL points back at the frontend. Mismatch either and you get CORS errors or failed fetches.

PHP bridge: same idea as a .env in a Laravel/Slim app — never commit secrets, read them at runtime. Next.js adds one twist: only NEXT_PUBLIC_-prefixed vars reach the browser.

7 · Build it

Your tangible win Assemble the full DocChat end to end and get the happy path working: sign up → upload a PDF → ask a question → see a grounded answer with its sources. Don't polish. Don't add features. Just make the whole chain run once, locally, from the browser. The day that single flow completes is the day you have a portfolio project.

Work the milestones in order. A checklist for the run:

1. Backend up   → uvicorn app.main:app --reload   → open /docs
2. Register a user in /docs → copy the returned JWT
3. Authorize in /docs → upload a small PDF → confirm chunks exist
4. Call /ask with a question → confirm a grounded answer comes back
5. Frontend up  → npm run dev → log in from the UI (CORS must pass)
6. Upload + ask from the UI → answer + sources render
7. Commit. You have a working capstone.

If a step fails, the drills page walks the exact breakages — CORS, 401s, empty retrieval, env mismatch — and how to trace each one.

8 · Check yourself

These test whether you understand the architecture, not just the code. Answer from memory.

Recall quiz

Which boundary is where CORS is configured?

Where do the document embeddings actually live?

What does the JWT carry to scope queries safely?

Which piece should you wire and prove up first?

In the "ask" flow, what happens just before generation?

Primary source ⭐ The 8-Week Roadmap is your integration map — it shows where every piece of DocChat came from and how this capstone threads through all eight weeks. Re-read the capstone box at the top; you've now built the whole thing.